# Path: generators/multi_ops.py
# @noglobals
# def multi_ops(data_stream, *funcs):
#     """ fork a generator with multiple operations/functions
# 
#         data_stream  -  an iterable data structure (ie: list/generator/tuple)
#         funcs        -  every function that will be applied to the data_stream """
# 
#     assert all(callable(func) for func in funcs), 'multi_ops can only apply functions to the first argument'
#     assert len(funcs), 'multi_ops needs at least one function to apply to data_stream'
# 
#     for i in data_stream:
#         if len(funcs) > 1:
#             yield tuple(func(i) for func in funcs)
#         elif len(funcs) == 1:
#             yield funcs[0](i)
# 
# Path: generators/window.py
# @strict_globals(deque=deque, islice=islice)
# def window(iterable, size=2):
#     ''' yields wondows of a given size '''
#     iterable = iter(iterable)
#     d = deque(islice(iterable, size-1), maxlen=size)
#     for _ in map(d.append, iterable):
#         yield tuple(d)


from .multi_ops import multi_ops
from .window import window
from strict_functions import noglobals, strict_globals
def map(*args):
    """ this map works just like the builtin.map, except, this one you can also:
        - give it multiple functions to map over an iterable
        - give it a single function with multiple arguments to run a window
          based map operation over an iterable
    """

    functions_to_apply = [i for i in args if callable(i)]
    iterables_to_run = [i for i in args if not callable(i)]
    #print('functions_to_apply:',functions_to_apply)
    #print('iterables_to_run:',iterables_to_run)

    assert len(functions_to_apply)>0, 'at least one function needs to be given to map'
    assert len(iterables_to_run)>0, 'no iterables were given to map'

    # check for native map usage
    if len(functions_to_apply) == 1 and len(iterables_to_run) >= 1 and function_arg_count(*functions_to_apply)==1:
        if hasattr(iter([]), '__next__'): # if python 3
            return __builtins__.map(functions_to_apply[0], *iterables_to_run)
        else:
            return iter(__builtins__.map(functions_to_apply[0], *iterables_to_run))
    # ---------------------------- new logic below ----------------------------
    # logic for a single function
    elif len(functions_to_apply) == 1:
        fn = functions_to_apply[0]
        # if there is a single iterable, chop it up
        if len(iterables_to_run) == 1:
            return (fn(*i) for i in window(iterables_to_run[0], function_arg_count(functions_to_apply[0])))
    # logic for more than 1 function
    elif len(functions_to_apply) > 1 and len(iterables_to_run) == 1:

# Path: generators/multi_ops.py
# @noglobals
# def multi_ops(data_stream, *funcs):
#     """ fork a generator with multiple operations/functions
# 
#         data_stream  -  an iterable data structure (ie: list/generator/tuple)
#         funcs        -  every function that will be applied to the data_stream """
# 
#     assert all(callable(func) for func in funcs), 'multi_ops can only apply functions to the first argument'
#     assert len(funcs), 'multi_ops needs at least one function to apply to data_stream'
# 
#     for i in data_stream:
#         if len(funcs) > 1:
#             yield tuple(func(i) for func in funcs)
#         elif len(funcs) == 1:
#             yield funcs[0](i)
# 
# Path: generators/window.py
# @strict_globals(deque=deque, islice=islice)
# def window(iterable, size=2):
#     ''' yields wondows of a given size '''
#     iterable = iter(iterable)
#     d = deque(islice(iterable, size-1), maxlen=size)
#     for _ in map(d.append, iterable):
#         yield tuple(d)


# Path: generators/map.py
from .multi_ops import multi_ops
from .window import window
from strict_functions import noglobals, strict_globals

def map(*args):
    """ this map works just like the builtin.map, except, this one you can also:
        - give it multiple functions to map over an iterable
        - give it a single function with multiple arguments to run a window
          based map operation over an iterable
    """

    functions_to_apply = [i for i in args if callable(i)]
    iterables_to_run = [i for i in args if not callable(i)]
    #print('functions_to_apply:',functions_to_apply)
    #print('iterables_to_run:',iterables_to_run)

    assert len(functions_to_apply)>0, 'at least one function needs to be given to map'
    assert len(iterables_to_run)>0, 'no iterables were given to map'

    # check for native map usage
    if len(functions_to_apply) == 1 and len(iterables_to_run) >= 1 and function_arg_count(*functions_to_apply)==1:
        if hasattr(iter([]), '__next__'): # if python 3
            return __builtins__.map(functions_to_apply[0], *iterables_to_run)
        else:
            return iter(__builtins__.map(functions_to_apply[0], *iterables_to_run))
    # ---------------------------- new logic below ----------------------------
    # logic for a single function
    elif len(functions_to_apply) == 1:
        fn = functions_to_apply[0]
        # if there is a single iterable, chop it up
        if len(iterables_to_run) == 1:
            return (fn(*i) for i in window(iterables_to_run[0], function_arg_count(functions_to_apply[0])))
    # logic for more than 1 function
    elif len(functions_to_apply) > 1 and len(iterables_to_run) == 1:

        return multi_ops(*(iterables_to_run + functions_to_apply))