INDENT_CHAR = '   '


def format_iterable(iterable,
                    exclude=None,
                    indent: int = 1) -> str:
    """
    Recursive formatting of an iterable, producing log-safe string

    Arguments:
        iterable (list, tuple, set, dict):
            iterable to sort
        exclude (list, optional):
            keys to exclude
        indent (int, optional):
            indent level for recursion

    Returns:
        (str):
            newline separated string
    """
    def treat_dict(inp):
        ret = []
        for a in sorted(inp.keys()):
            v = inp[a]

            if a not in exclude:
                if type(v) in dispatch.keys():
                    v = format_iterable(iterable=v,
                                        exclude=exclude,
                                        indent=indent + 1)
                ret.append(INDENT_CHAR * indent + f'{a}: {v}')

        return '\n' + '\n'.join(ret)

    def treat_iterable(inp):
        exclude.append('')

        ret = []
        for v in inp:

            if v not in exclude:
                if type(v) in dispatch.keys():
                    v = format_iterable(iterable=v,
                                        exclude=exclude,
                                        indent=indent + 1)
                ret.append(INDENT_CHAR * (indent-1) + f'- {v}')

        return f'\n' + f'\n'.join(ret)

    def treat_printable(inp):
        if inp is None:
            return 'None'
        return f'{inp}'

    # can hit empty lists (often in recursion)
    # output looks nicer if they're output without formatting
    try:
        if len(iterable) == 0:
            return '\n' + INDENT_CHAR * indent + f'{iterable}'
    except TypeError:
        return treat_printable(iterable)

    if exclude is None:
        exclude = []
    elif isinstance(exclude, str):
        exclude = [exclude]

    dispatch = {dict: treat_dict,
                list: treat_iterable,
                tuple: treat_iterable,
                set: treat_iterable,
                str: treat_printable,
                type: treat_printable,
                }
    return dispatch[type(iterable)](iterable)
