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

"""Show or convert the configuration of an SCons cache directory.

A cache of derived files is stored by file signature.
The files are split into directories named by the first few
digits of the signature. The prefix length used for directory
names can be changed by this script.
"""

import argparse
import glob
import json
import os

def rearrange_cache_entries(current_prefix_len, new_prefix_len) -> None:
    """Move cache files if prefix length changed.

    Move the existing cache files to new directories of the
    appropriate name length and clean up the old directories.
    """
    print('Changing prefix length from', current_prefix_len,
          'to', new_prefix_len)
    dirs = set()
    old_dirs = set()
    for file in glob.iglob(os.path.join('*', '*')):
        name = os.path.basename(file)
        dname = name[:current_prefix_len].upper()
        if dname not in old_dirs:
            print('Migrating', dname)
            old_dirs.add(dname)
        dname = name[:new_prefix_len].upper()
        if dname not in dirs:
            os.mkdir(dname)
            dirs.add(dname)
        os.rename(file, os.path.join(dname, name))

    # Now delete the original directories
    for dname in old_dirs:
        os.rmdir(dname)


# The configuration dictionary should have one entry per entry in the
# cache config. The value of each entry should include the following:
#   implicit - (optional) This is to allow adding a new config entry and also
#              changing the behaviour of the system at the same time. This
#              indicates the value the config entry would have had if it had
#              been specified.
#   default - The value the config entry should have if it wasn't previously
#             specified
#   command-line - parameters to pass to ArgumentParser.add_argument
#   converter - (optional) Function to call if conversion is required
#               if this configuration entry changes
config_entries = {
    'prefix_len': {
        'implicit': 1,
        'default': 2,
        'command-line': {
            'help': 'Length of cache file name used as subdirectory prefix',
            'metavar': '<number>',
            'type': int
        },
        'converter': rearrange_cache_entries
    }
}


def main():
    parser = argparse.ArgumentParser(
        description='Modify the configuration of an scons cache directory',
        epilog='''
               Unspecified options will not be changed unless they are not
               set at all, in which case they are set to an appropriate default.
               ''')

    parser.add_argument('cache-dir', help='Path to scons cache directory')
    for param in config_entries:
        parser.add_argument('--' + param.replace('_', '-'),
                            **config_entries[param]['command-line'])
    parser.add_argument('--version',
                        action='version',
                        version='%(prog)s 1.0')
    parser.add_argument('--show',
                        action="store_true",
                        help="show current configuration")

    # Get the command line as a dict without any of the unspecified entries.
    args = dict([x for x in vars(parser.parse_args()).items() if x[1]])

    # It seems somewhat strange to me, but positional arguments don't get the -
    # in the name changed to _, whereas optional arguments do...
    cache = args['cache-dir']
    if not os.path.isdir(cache):
        raise RuntimeError("There is no cache directory named %s" % cache)
    os.chdir(cache)
    del args['cache-dir']

    if not os.path.exists('config'):
        # old config dirs did not have a 'config' file. Try to update.
        # Validate the only files in the directory are directories 0-9, a-f
        expected = [f'{x:X}' for x in range(0, 16)]
        if not set(os.listdir('.')).issubset(expected):
            raise RuntimeError(
                "%s does not look like a valid version 1 cache directory" % cache)
        config = dict()
    else:
        with open('config') as conf:
            config = json.load(conf)

    if args.get('show', None):
        print("Current configuration in '%s':" % cache)
        print(json.dumps(config, sort_keys=True,
                         indent=4, separators=(',', ': ')))
        # in case of the show argument, emit some stats as well
        file_count = 0
        for _, _, files in os.walk('.'):
            file_count += len(files)
        if file_count:  # skip config file if it exists
            file_count -= 1
        print("Cache contains %s files" % file_count)
        del args['show']

    # Find any keys that are not currently set but should be
    for key in config_entries:
        if key not in config:
            if 'implicit' in config_entries[key]:
                config[key] = config_entries[key]['implicit']
            else:
                config[key] = config_entries[key]['default']
            if key not in args:
                args[key] = config_entries[key]['default']

    # Now go through each entry in args to see if it changes an existing config
    # setting.
    for key in args:
        if args[key] != config[key]:
            if 'converter' in config_entries[key]:
                config_entries[key]['converter'](config[key], args[key])
            config[key] = args[key]

    # and write the updated config file
    with open('config', 'w') as conf:
        json.dump(config, conf)

if __name__ == "__main__":
    main()
