from typing import Callable, Dict, Iterable, Iterator, List, Tuple, TypeVar, Union

from functools import partial
from itertools import chain
from itertools import islice
from itertools import repeat
from itertools import starmap

X = TypeVar("X")
Y = TypeVar("Y")
Z = TypeVar("Z")

Option = Union[Tuple[X], Tuple]
Either = Tuple[Option[X], Option[Y]]

AnyFunction = Callable[[Tuple[object, ...], Dict[str, object]], object]
OptionFunction = Callable[[Tuple[object, ...], Dict[str, object]], Option[object]]

def binary_compose(f: Callable[[Y], Z], g: Callable[[X], Y]) -> Callable[[X], Z]:
  return lambda x: f(g(x))

def functor(f: Callable[[X], Y], x: Option[X]) -> Option[Y]:
  y: Iterable[Y] = map(f, x)
  return tuple(y)

def monad(f: Callable[[X], Option[Y]], x: Option[X]) -> Option[Y]:
  ys: Iterable[Option[Y]] = map(f, x)
  y: Iterable[Y] = chain.from_iterable(ys)

  return tuple(y)

def wraptry(f: AnyFunction) -> OptionFunction:
  def wrapped(*args: object, **kwargs: object) -> Option[object]:
    try:
      return (f(*args, **kwargs),)
    except:
      return ()

  return wrapped

def maptry(f: Callable[[X], Y], xs: Iterable[X]) -> Iterable[Y]:
  ys: Iterable[Option[Y]] =  map(wraptry(f), xs)
  return chain.from_iterable(ys)

def wrapnext(xs: Iterator[X]) -> Option[X]:
  return wraptry(next)(xs)

def wrapeek(xs: Iterable[X]) -> Tuple[Option[X], Iterable[X]]:
  xs = iter(xs)
  x = wrapnext(xs)

  return x, chain(x, xs)

def iterfind(fs: Iterable[Callable[[X], bool]], xs: Iterable[X]) -> Iterable[Option[X]]:
  xs = iter(xs)
  buffer = []

  for f in fs:
    x = find(f, buffer)

    if not x:
      x, buffer = find_and_collect(f, xs, buffer)

    yield x

def find(f: Callable[[X], bool], xs: Iterable[X]) -> Option[X]:
  xs = filter(f, xs)
  return wrapnext(xs)

def find_and_collect(
    f: Callable[[X], bool],
    xs: Iterator[X],
    buffer: List[X]
    ) -> Tuple[Option[X], List[X]]:

  x = wrapnext(xs)

  while x:
    buffer.extend(x)

    if f(*x):
      return x, buffer

    x = wrapnext(xs)

  return (), buffer

def findindex(f: Callable[[X], bool], xs: Iterable[X]) -> Option[int]:
  yxs = enumerate(xs)

  g: Callable[[Tuple[int, X]], bool] = binary_compose(f, first)
  yx: Option[Tuple[int, X]] = find(g, yxs)

  return functor(first, yx)

def first(xy: Tuple[X, Y]) -> X:
  return xy[0]

def second(xy: Tuple[X, Y]) -> Y:
  return xy[1]

def mapexplode(f: Callable[[X, Y], Z], x: X, ys: Iterable[Y]) -> Iterable[Z]:
  xs_and_ys: Iterable[Tuple[X, Y]] = explode(x, ys)
  return starmap(f, xs_and_ys)

def explode(x: X, ys: Iterable[Y]) -> Iterable[Tuple[X, Y]]:
  return zip(repeat(x), ys)

def cachepartial(f: AnyFunction, *args: object, **kwargs: object) -> AnyFunction:
  f = partial(f, *args, **kwargs)
  return cache(f)

def cache(f: AnyFunction) -> AnyFunction:
  def cached() -> Iterable[object]:
    args, kwargs = yield
    y = f(*args, **kwargs)

    while True:
      yield y

  cached = cached()
  next(cached)

  def call(*args: object, **kwargs: object) -> object:
    return cached.send((args, kwargs))

  return call

def slide(xs: Iterable[X], length: int = 2, step: int = 1) -> Iterable[Tuple[X, ...]]:
  xs = iter(xs)

  window = islice(xs, length)
  window = tuple(window)

  while len(window) > 0:
    yield window

    window = chain(window[step:], islice(xs, step))
    window = tuple(window)
