'''
The purpose of this file is to work with JSON-based config files, where you
load a default configuration, then overlay a user-supplied configuration on
top, overwriting the matching keys and keeping default values for the rest.
The functions will then suggest that the user config needs to be re-saved if
the default key set contains keys that the user key set does not.
'''
import copy
import json

from voussoirkit import pathclass

def recursive_dict_keys(d):
    '''
    Given a dictionary, return a set containing all of its keys and the keys of
    all other dictionaries that appear as values within. The subkeys will use \\
    to indicate their lineage.

    {'hi': {'ho': 'neighbor'}}

    returns

    {'hi', 'hi\\ho'}
    '''
    keys = set(d.keys())
    for (key, value) in d.items():
        if isinstance(value, dict):
            subkeys = {f'{key}\\{subkey}' for subkey in recursive_dict_keys(value)}
            keys.update(subkeys)
    return keys

def recursive_dict_update(target, supply):
    '''
    Update target using supply, but when the value is a dictionary update the
    insides instead of replacing the dictionary itself. This prevents keys that
    exist in the target but don't exist in the supply from being erased.
    Note that we are modifying target in place.

    eg:
    target = {'hi': 'ho', 'neighbor': {'name': 'Wilson'}}
    supply = {'neighbor': {'behind': 'fence'}}

    result: {'hi': 'ho', 'neighbor': {'name': 'Wilson', 'behind': 'fence'}}
    whereas a regular dict.update would have produced:
    {'hi': 'ho', 'neighbor': {'behind': 'fence'}}
    '''
    for (key, value) in supply.items():
        if isinstance(value, dict):
            existing = target.get(key, None)
            if existing is None:
                target[key] = value
            else:
                recursive_dict_update(target=existing, supply=value)
        else:
            target[key] = value

def layer_json(target, supply):
    '''
    target is the dictionary into which the final values will be placed
    (presumably loaded from a default set), and supply is a layer being applied
    on top of the target (presumably a user-supplied set). needs_rewrite is
    True if the target contains keys that the supply did not, indicating that
    the supply is incomplete.
    '''
    target_keys = recursive_dict_keys(target)
    supply_keys = recursive_dict_keys(supply)
    needs_rewrite = len(target_keys.difference(supply_keys)) > 0
    recursive_dict_update(target=target, supply=supply)
    return (target, needs_rewrite)

def load_file(filepath, default_config):
    '''
    Given a filepath to a user-supplied config file, and a dict of default
    values, return a new dict containing the user-supplied values overlaid onto
    the defaults, and needs_rewrite indicating that the user config is missing
    some keys from the default config.
    '''
    path = pathclass.Path(filepath)
    user_config_exists = path.exists

    # This config will hold the final values. We start by loading it with the
    # defaults, so that as we go through the user's config we can overwrite the
    # user-specified keys, and the keys which the user does not specify will
    # remain default.
    final_config = copy.deepcopy(default_config)
    needs_rewrite = False

    if user_config_exists:
        with path.open('r', encoding='utf-8') as handle:
            user_config = json.load(handle)
        (final_config, needs_rewrite) = layer_json(target=final_config, supply=user_config)
    else:
        needs_rewrite = True

    return (final_config, needs_rewrite)

'''
The purpose of this file is to work with JSON-based config files, where you
load a default configuration, then overlay a user-supplied configuration on
top, overwriting the matching keys and keeping default values for the rest.
The functions will then suggest that the user config needs to be re-saved if
the default key set contains keys that the user key set does not.
'''
import copy
import json

from voussoirkit import pathclass

def recursive_dict_keys(d):
    '''
    Given a dictionary, return a set containing all of its keys and the keys of
    all other dictionaries that appear as values within. The subkeys will use \\
    to indicate their lineage.

    {'hi': {'ho': 'neighbor'}}

    returns

    {'hi', 'hi\\ho'}
    '''
    keys = set(d.keys())
    for (key, value) in d.items():
        if isinstance(value, dict):
            subkeys = {f'{key}\\{subkey}' for subkey in recursive_dict_keys(value)}
            keys.update(subkeys)
    return keys

def recursive_dict_update(target, supply):
    '''
    Update target using supply, but when the value is a dictionary update the
    insides instead of replacing the dictionary itself. This prevents keys that
    exist in the target but don't exist in the supply from being erased.
    Note that we are modifying target in place.

    eg:
    target = {'hi': 'ho', 'neighbor': {'name': 'Wilson'}}
    supply = {'neighbor': {'behind': 'fence'}}

    result: {'hi': 'ho', 'neighbor': {'name': 'Wilson', 'behind': 'fence'}}
    whereas a regular dict.update would have produced:
    {'hi': 'ho', 'neighbor': {'behind': 'fence'}}
    '''
    for (key, value) in supply.items():
        if isinstance(value, dict):
            existing = target.get(key, None)
            if existing is None:
                target[key] = value
            else:
                recursive_dict_update(target=existing, supply=value)
        else:
            target[key] = value

def layer_json(target, supply):
    '''
    target is the dictionary into which the final values will be placed
    (presumably loaded from a default set), and supply is a layer being applied
    on top of the target (presumably a user-supplied set). needs_rewrite is
    True if the target contains keys that the supply did not, indicating that
    the supply is incomplete.
    '''
    target_keys = recursive_dict_keys(target)
    supply_keys = recursive_dict_keys(supply)
    needs_rewrite = len(target_keys.difference(supply_keys)) > 0
    recursive_dict_update(target=target, supply=supply)
    return (target, needs_rewrite)

def load_file(filepath, default_config):
    '''
    Given a filepath to a user-supplied config file, and a dict of default
    values, return a new dict containing the user-supplied values overlaid onto
    the defaults, and needs_rewrite indicating that the user config is missing
    some keys from the default config.
    '''
    path = pathclass.Path(filepath)
    user_config_exists = path.exists

    # This config will hold the final values. We start by loading it with the
    # defaults, so that as we go through the user's config we can overwrite the
    # user-specified keys, and the keys which the user does not specify will
    # remain default.
    final_config = copy.deepcopy(default_config)
    needs_rewrite = False

    if user_config_exists:
        with path.open('r', encoding='utf-8') as handle:
            user_config = json.load(handle)
        (final_config, needs_rewrite) = layer_json(target=final_config, supply=user_config)
    else:
        needs_rewrite = True

    return (final_config, needs_rewrite)

The purpose of this file is to work with JSON-based config files, where you load a default configuration, then overlay a user-supplied configuration on top, overwriting the matching keys and keeping default values for the rest. The functions will then suggest that the user config needs to be re-saved if the default key set contains keys that the user key set does not. Given a dictionary, return a set containing all of its keys and the keys of     all other dictionaries that appear as values within. The subkeys will use \\     to indicate their lineage.      {'hi': {'ho': 'neighbor'}}      returns      {'hi', 'hi\\ho'} Update target using supply, but when the value is a dictionary update the     insides instead of replacing the dictionary itself. This prevents keys that     exist in the target but don't exist in the supply from being erased.     Note that we are modifying target in place.      eg:     target = {'hi': 'ho', 'neighbor': {'name': 'Wilson'}}     supply = {'neighbor': {'behind': 'fence'}}      result: {'hi': 'ho', 'neighbor': {'name': 'Wilson', 'behind': 'fence'}}     whereas a regular dict.update would have produced:     {'hi': 'ho', 'neighbor': {'behind': 'fence'}} target is the dictionary into which the final values will be placed     (presumably loaded from a default set), and supply is a layer being applied     on top of the target (presumably a user-supplied set). needs_rewrite is     True if the target contains keys that the supply did not, indicating that     the supply is incomplete. Given a filepath to a user-supplied config file, and a dict of default     values, return a new dict containing the user-supplied values overlaid onto     the defaults, and needs_rewrite indicating that the user config is missing     some keys from the default config. # This config will hold the final values. We start by loading it with the # defaults, so that as we go through the user's config we can overwrite the # user-specified keys, and the keys which the user does not specify will # remain default.