"""Utility functions."""

from collections.abc import Mapping
from typing import Any


def merge_dict_nested(
    dic_user: Mapping[str, Any], dic_default: dict[str, Any]
) -> dict[str, Any]:
    """
    Merge two dictionaries.

    Keep elements from `dic_user` if it exists, with a fallback on elements in
    `dic_default`. All nested dictionaries are also treated like this.

    Parameters
    ----------
    dic_user : dict-like
        Dict being merged, its values have priority over `dic_default`.
    dic_default : dict-like
        Dict being merged, its values are used if they are not in `dic_user`.

    Returns
    -------
    dic : dict
        Merged dict.
    """
    dic_copy = dic_default.copy()
    for key, value in dic_user.items():
        if key in dic_default:
            if isinstance(value, Mapping):
                dic_copy[key] = merge_dict_nested(value, dic_default[key])
            else:
                if key in dic_user:
                    dic_copy[key] = dic_user[key]

        else:
            dic_copy[key] = dic_user[key]

    return dic_copy


def cast_type_dict(
    dic: dict[str, Any], in_type: type, out_type: type
) -> dict[str, Any]:
    """
    Convert `in_type` value to `out_type` values in a dictionary.

    Nested dictionaries are supported, so the casted types can't be `dict`.

    Parameters
    ----------
    dic : dict
        Dict in which values are casted.
    in_type, out_type : type
        `in_type` types are casted to `out_type`. Can't be `dict`.
    """
    dic_copy = dic.copy()
    for key, value in dic.items():
        if isinstance(value, dict):
            dic_copy[key] = cast_type_dict(value, in_type, out_type)
        else:
            if isinstance(dic_copy[key], in_type):
                dic_copy[key] = out_type(dic[key])

    return dic_copy


def strip_none_dict(dic: dict) -> dict:
    """
    Strip elements that are None from `dic`.

    Parameters
    ----------
    dic : dict
        Dict in which None values are removed.

    Returns
    -------
    dic_stripped : dict
        Input dictionary where None values were removed.
    """
    dic_copy = dic.copy()
    for key, value in dic.items():
        if isinstance(value, dict):
            dic_copy[key] = strip_none_dict(value)
        elif value is None:
            dic_copy.pop(key)

    return dic_copy


def remove_duplicate_values(
    dic1: dict[str, Any], dic2: dict[str, Any]
) -> dict[str, Any]:
    """
    Remove entries with duplicate value from `dic1` if it is in `dic2`.

    The entry in `dic1` is only removed if it has another entry with the same value
    (whatever its key).

    Parameters
    ----------
    dic1 : dict
        Dictionary in which entries with duplicate value are removed.
    dic2 : dict
        Dictionary checked against `dic1`.

    Returns
    -------
    dic : dict
        Same as `dic1`, with entries whose value are found in `dic2` were removed.
    """
    # Reverse dict to check duplicate values
    dic1_reverse = dict()
    for key, value in dic1.items():
        # Supply an empty list for missing key
        dic1_reverse.setdefault(value, list()).append(key)

    # List keys that have duplicate values
    duplicates = flatten_list(
        [values for key, values in dic1_reverse.items() if len(values) > 1]
    )

    # Remove entries with duplicate values that are in dic2
    dic1_copy = dic1.copy()
    for key in duplicates:
        if key in dic2 and dic1[key] == dic2[key]:
            dic1_copy.pop(key)

    return dic1_copy


def flatten_list(list_of_lists: list[list[Any]]) -> list[Any]:
    """
    Flatten a list of lists.

    Parameters
    ----------
    list_of_lists : list of lists
        The list of lists to flatten.

    Returns
    -------
    flat_list : list
        Flat list.
    """
    flat_list = []
    for lst in list_of_lists:
        flat_list += lst
    return flat_list
