from itertools import islice
from typing import Iterable, Callable, Sequence, Generator, TypeVar, Tuple


def chain_call(fns: Iterable[Callable], *initial_call_arguments):
    """
    Simple recipe to chain call functions with matching input and output arguments.
    """
    arguments_signature = len(initial_call_arguments)
    if arguments_signature < 1:
        raise Exception("Cannot chain functions without input")
    elif arguments_signature == 1:
        arg = initial_call_arguments[0]
        for fn in fns:
            arg = fn(arg)
        return arg
    # unpack multiple arguments for calls
    args = initial_call_arguments
    for fn in fns:
        args = fn(*args)
    return args


_SlicedSeqType = TypeVar("_SlicedSeqType")


def multi_islice(
    seq: Sequence[_SlicedSeqType], slices: Sequence[int], start: int, end: int
) -> Generator[Iterable[_SlicedSeqType], None, None]:
    """
    Slice up a sequence with iterators at places determined by a range from another sequence.
    """
    assert end >= start >= 0
    assert len(slices) >= start
    x = 0 if start == 0 else slices[start - 1]
    for i in range(start, min(end, len(slices))):
        t = slices[i]
        yield islice(seq, x, t)
        x = t
    if end > len(slices):
        yield islice(seq, x, len(seq))


def encode_interleaved_int(a: int, b: int) -> int:
    assert a >= 0 <= b
    o = 0
    m = 1
    for i in range(max(a.bit_length(), b.bit_length())):
        o |= (((a & m) << 1) | (b & m)) << i
        m <<= 1
    return o


def decode_interleaved_int(x: int) -> Tuple[int, int]:
    assert x >= 0
    a = 0
    b = 0
    for i in range((x.bit_length() + 1) // 2):
        a |= ((x & 2) >> 1) << i
        b |= (x & 1) << i
        x >>= 2
    return a, b
