"""
Custom data types and functions for manipulating data types
"""

from collections.abc import MutableMapping
from typing import Any, List, Optional, Tuple, Union

from box import Box

DictLike = Union[dict, Box]


class CompoundKey(tuple):
    ...


class Config(Box):
    def copy(self) -> "Config":
        """
        Creates a recursive copy of the configuration instance.
        Each level of configuration (if there are nested configs)
        is a separate `Config` instance, and each level (and overwriting or modifying)
        does not impact other levels. However, values aren't deep copied,
        so modifications to mutable objects shared between levels
        or keys will impacts all levels / keys.

        Returns:
            - Config: New (shallow) copy of the configuration
        """

        new_config = Config()
        for key, value in self.items():
            if isinstance(value, Config):
                value = value.copy()

            new_config[key] = value

        return new_config


def merge_dicts(d1: DictLike, d2: DictLike) -> DictLike:
    """
    Updates `d1` from `d2` by replacing each `(k, v1)` pair in `d1` with the
    corresponding `(k, v2)` pair in `d2`.
    If the value of each pair is itself a dict, then the value is updated
    recursively.

    Args:
        - d1 (MutableMapping): A dictionary to be replaced
        - d2 (MutableMapping): A dictionary used for replacement

    Returns:
        - A `MutableMapping` with the two dictionary contents merged
    """

    new_dict = d1.copy()

    for k, v in d2.items():
        if isinstance(new_dict.get(k), MutableMapping) and isinstance(
            v, MutableMapping
        ):
            new_dict[k] = merge_dicts(new_dict[k], d2[k])
        else:
            new_dict[k] = d2[k]
    return new_dict


def dict_to_flatdict(dct: dict, parent: CompoundKey = None) -> dict:
    """
    Converts a (nested) dictionary to a flattened representation.
    Each key of the flat dict will be a CompoundKey tuple containing the "chain of keys"
    for the corresponding value.

    Args:
        - dct (dict): The dictionary to flatten
        - parent (CompoundKey, optional): Defaults to `None`. The parent key
        (you shouldn't need to set this)

    Returns:
        - dict: A flattened dict
    """

    items: List[Tuple[CompoundKey, Any]] = []
    parent = parent or CompoundKey()
    for k, v in dct.items():
        k_parent = CompoundKey(parent + (k,))
        if isinstance(v, dict):
            items.extend(dict_to_flatdict(v, parent=k_parent).items())
        else:
            items.append((k_parent, v))
    return dict(items)


def flatdict_to_dict(dct: dict, dct_class: Optional[type] = None) -> MutableMapping:
    """
    Converts a flattened dictionary back to a nested dictionary.

    Args:
        - dct (dict): The dictionary to be nested. Each key should be a
        `CompoundKey`, as generated by `dict_to_flatdict()`
        - dct_class (type, optional): the type of the result; defaults to `dict`

    Returns:
        - MutableMapping: A `MutableMapping` used to represent a nested dictionary
    """

    result: MutableMapping = (dct_class or dict)()
    for k, v in dct.items():
        if isinstance(k, CompoundKey):
            current_dict = result
            for ki in k[:-1]:
                current_dict = current_dict.setdefault(  # type: ignore
                    ki, (dct_class or dict)()
                )
            current_dict[k[-1]] = v
        else:
            result[k] = v

    return result
