import asyncio
from decimal import Decimal, InvalidOperation
from typing import Any, Callable, Iterable, NoReturn, Sequence, TypeVar, Union

from pnq._itertools.common import Listable, name_as
from pnq._itertools.op import MAP_ASSIGN_OP, TH_ASSIGN_OP, TH_ROUND
from pnq.exceptions import NoElementError, NotOneElementError
from pnq.selectors import starmap

from . import queries

T = TypeVar("T")


def to(source, finalizer):
    return finalizer(source)


@name_as("len")
def _len(source: Iterable[T]) -> int:
    count = 0
    for x in Listable(source, None):
        count += 1

    return count


def exists(source: Iterable[T], predicate=None) -> bool:
    for x in queries._filter(source, predicate):
        return True

    return False


def empty(source: Iterable[T], predicate=None) -> bool:
    for x in queries._filter(source, predicate):
        return False

    return True


@name_as("all")
def _all(source: Iterable[T], selector=None) -> bool:
    for x in Listable(source, selector):
        if not x:
            return False

    return True


@name_as("any")
def _any(source: Iterable[T], selector=None) -> bool:
    for x in Listable(source, selector):
        if x:
            return True

    return False


# containsより意思的
def find(source: Iterable[T], value, selector=None) -> bool:
    for val in Listable(source, selector):
        if val == value:
            return True

    return False


def contains(source: Iterable[T], value, selector=None) -> bool:
    for val in Listable(source, selector):
        if val == value:
            return True

    return False


@name_as("sum")
def _sum(source: Iterable[T], selector=None):
    current = 0
    for val in Listable(source, selector):
        current += val

    return current


@name_as("min")
def _min(source: Iterable[T], key_selector=None, default=NoReturn):
    if default is NoReturn:
        return min(Listable(source), key=key_selector)
    else:
        return min(Listable(source), key=key_selector, default=default)


@name_as("max")
def _max(source: Iterable[T], key_selector=None, default=NoReturn):
    if default is NoReturn:
        return max(Listable(source), key=key_selector)
    else:
        return max(Listable(source), key=key_selector, default=default)


def average(
    source: Iterable[T],
    selector=lambda x: x,
    exp: float = 0.00001,
    round: TH_ROUND = "ROUND_HALF_UP",
) -> Union[float, Decimal]:
    # import statistics
    # return statistics.mean(pmap(self, selector))  # type: ignore

    seed = Decimal("0")
    i = 0
    val = 0

    for val in Listable(source, selector):
        i += 1
        # val = selector(val)
        try:
            val = Decimal(str(val))  # type: ignore
        except InvalidOperation:
            raise TypeError(f"{val!r} is not a number")
        seed += val

    if i:
        result = seed / i
    else:
        result = seed

    result = result.quantize(Decimal(str(exp)), rounding=round)

    if isinstance(val, (int, float)):
        return float(result)
    else:
        return result


def reduce(
    source: Iterable[T],
    seed: Union[T, None],
    op: Union[TH_ASSIGN_OP, Callable[[Any, Any], Any]] = "+=",
    selector=lambda x: x,
) -> T:
    if isinstance(op, str):
        binary_op = MAP_ASSIGN_OP[op]
    else:
        binary_op = op

    it = Listable(source, selector).__iter__()

    if seed is None:
        for val in it:
            seed = val
            break
        else:
            raise TypeError("empty sequence with no seed.")

    for val in Listable(source, selector):
        seed = binary_op(seed, val)

    return seed  # type: ignore


def accumulate(
    source: Iterable[T],
    seed: Union[T, None],
    op: Union[TH_ASSIGN_OP, Callable[[Any, Any], Any]] = "+=",
    selector=lambda x: x,
):
    if isinstance(op, str):
        binary_op = MAP_ASSIGN_OP[op]
    else:
        binary_op = op

    it = Listable(source, selector).__iter__()

    results = []

    if seed is None:
        for val in it:
            seed = val
            results.append(seed)
            break
        else:
            return results
    else:
        results.append(seed)

    for val in it:
        seed = binary_op(seed, val)
        results.append(seed)

    return results


def concat(source: Iterable[T], selector=None, delimiter: str = ""):
    to_str = lambda x: "" if x is None else str(x)  # noqa
    return delimiter.join(Listable(Listable(source, selector), to_str))


def each(source: Iterable[T], func=lambda x: x, unpack=""):
    if asyncio.iscoroutinefunction(func):
        func = starmap(func, unpack)
        for elm in source:
            func(elm)
    else:
        func = starmap(func, unpack)
        for elm in source:
            func(elm)


def one(source: Iterable[T]):
    it = source.__iter__()
    try:
        result = it.__next__()
    except StopIteration:
        raise NoElementError()

    try:
        it.__next__()
        raise NotOneElementError()
    except StopIteration:
        pass

    return result


def first(source: Iterable[T]):
    it = source.__iter__()
    try:
        return it.__next__()
    except StopIteration:
        raise NoElementError()


def last(source: Iterable[T]):
    if isinstance(source, Sequence):
        try:
            return source[-1]
        except IndexError:
            raise NoElementError()

    undefined = object()
    last = undefined
    for elm in source:
        last = elm

    if last is undefined:
        raise NoElementError()
    else:
        return last


def one_or(source: Iterable[T], default):
    try:
        return one(source)
    except NoElementError:
        return default


def first_or(source: Iterable[T], default):
    try:
        return first(source)
    except NoElementError:
        return default


def last_or(source: Iterable[T], default):
    try:
        return last(source)
    except NoElementError:
        return default


def one_or_raise(source: Iterable[T], exc: Union[str, Exception]):
    undefined = object()
    result = one_or(source, undefined)
    if result is undefined:
        if isinstance(exc, str):
            raise Exception(exc)
        else:
            raise exc
    else:
        return result


def first_or_raise(source: Iterable[T], exc: Union[str, Exception]):
    undefined = object()
    result = first_or(source, undefined)
    if result is undefined:
        if isinstance(exc, str):
            raise Exception(exc)
        else:
            raise exc
    else:
        return result


def last_or_raise(source: Iterable[T], exc: Union[str, Exception]):
    undefined = object()
    result = last_or(source, undefined)
    if result is undefined:
        if isinstance(exc, str):
            raise Exception(exc)
        else:
            raise exc
    else:
        return result


def to_dict(key_values):
    if hasattr(key_values, "__iter__"):
        result = {}
        for k, v in key_values:
            result[k] = v
        return result
    elif hasattr(key_values, "__iter__"):
        return dict(key_values)
    else:
        raise TypeError(f"{key_values} no has __iter__ or __aiter__")
