from typing import Optional
from typing import Mapping
from typing import List
from typing import Any
import itertools
import logging


def generate_experiments(
    grid_params: Optional[Mapping[str, List[Any]]] = None,
    explicit: Optional[List[Mapping[str, Any]]] = None,
) -> List[Mapping[str, Any]]:
    """
    Generate experiments for a parameter grid.

    :param grid_params:
        The grid parameters in format key -> list of choices
    :param explicit:
        Explicit experiment to add in form list(key -> value)
    :return:
        One list per worker.
    """
    # Default values
    if grid_params is None:
        grid_params = {}
    if explicit is None:
        explicit = []

    # Create search list for grid
    params = sorted(grid_params.keys())
    value_ranges = []
    for k in params:
        v = grid_params[k]
        if not isinstance(v, list) or isinstance(v, tuple):
            v = [v]
        value_ranges.append(v)
    search_list = [dict(zip(params, values)) for values in itertools.product(*value_ranges)]
    logging.info(f'Created grid search with {len(search_list)} runs.')

    # Add explicit runs
    if len(explicit) > 0:
        search_list += explicit
        logging.info(f'Added {len(explicit)} explicit runs.')

    # Consistent order
    return sorted(search_list, key=lambda config: tuple(hash(p) for p in sorted(config.items())))