###############################################################################
# Copyright 2018 The AnPyLar Team. All Rights Reserved.
# Use of this source code is governed by an MIT-style license that
# can be found in the LICENSE file at http://anpylar.com/mit-license
###############################################################################
from .observable_base import (ObservableOperator, _MetaObservableOperator,
                              Disposable, ObservableStopError)

from .utils import defaultdict

__all__ = []


class All_Operator(ObservableOperator):
    '''
    Check if all items generated by the Observable meet the condition
    determined by ``predicate``
    '''

    def __init__(self, predicate):
        self._fn = predicate
        self._result = True

    def on_next(self, val, sid):
        if self._result:
            self._result = self._fn(val)

    def on_completed(self, sid):
        self._next(self._result, sid)
        super().on_completed(sid)


class Catch_Exception_Operator(ObservableOperator):
    '''
    Swicth to another observable if an error has been produced and ``on_error``
    would be invoked in the subscriber.

      - If ``handler`` is an *Observable*, it will be forwarded to it

      - Else, ``handler`` must be a callable accepting the value and returning
        the value to switch to for forwarding

      - If the forwarded value is an *Observable* it will be the result to
        switch to

      - Else, the forwarded value will be converted using ``Observable.of``
    '''

    def __init__(self, handler):
        self._handler = handler
        self._isobs = isinstance(handler, Observable)
        self._active = defaultdict(bool)
        # End of chain, trigger a fake subscription
        # self.subscribe(Subscription(self))  # trigger

    def on_error(self, error, sid):
        # remove signaling that error happened
        if self._active[sid]:
            super.on_error(error, sid)
            return

        self._active[sid] = True
        self._error_old, self._error = self._error, None
        if self._isobs:
            obs = self._handler
        else:
            obs = self._handler(error)
            if not isinstance(obs, Observable):
                obs = Observable.of(obs)

        # Internal subscription, for notified sid
        # self._delay_next(sid, lambda: obs._subscribe(self, sid))
        obs._subscribe(self, sid)


class Debounce_Operator(ObservableOperator):
    '''
    Delay the generated value by the amount of milliseconds ``ms`` and discard
    the delayed value if a new one is produced.
    '''

    _timed_op = True

    def __init__(self, ms):
        self.tout = ms
        self.timers = defaultdict(int)

    def _bouncer(self, val, sid):
        self.timers[sid] = None
        self._next(val, sid)

    def on_next(self, val, sid):
        if self.timers[sid]:
            self._delay_stop(sid, self.timers[sid])

        self.timers[sid] = \
            self._delay_next(sid, lambda: self._bouncer(val, sid), self.tout)


class DefVal_Operator(ObservableOperator):
    '''
    This is a particular implementation in *AnPyLar*. It provides a default
    value for the Observable + Chain of Operators, intended for elements that
    for example subscribe in the background (like html nodes do)

    It is intended to be used as the last operator in a chain (although with
    care it can be used earlier in the chain)
    '''

    def __init__(self, defval):
        self.defval = defval

    def subscribe(self, *args, **kwargs):
        disp = super().subscribe(*args, **kwargs)
        disp.val = self.defval
        return disp


class Delay_Operator(ObservableOperator):
    '''
    Delay the generated value by ``ms`` milliseconds
    '''
    _timed_op = True

    def __init__(self, ms):
        self.tout = ms

    def on_next(self, val, sid):
        self._delay_next(sid, lambda: self._next(val, sid), self.tout)


class Distinct_Operator(ObservableOperator):
    '''
    Let the value through if it has not been seen before. If ``predicate`` is
    provided it will be used to assess if a value is distinct from previous
    values

    .. note:: Use with care, because on long running observables, the buffer
              will grow with no limits.
    '''

    def __init__(self, predicate=None):
        self._fn = predicate
        self._lookup = defaultdict(set)

    def _operate(self, val, sid):
        fn = self._fn
        if fn is None:
            if val in self._lookup[sid]:
                raise ObservableStopError()

        elif any(fn(v, val) for v in self._lookup[sid]):
            raise ObservableStopError()

        self._lookup[sid].add(val)  # not found, add and progress
        return val


class Distinct_Until_Changed_Operator(ObservableOperator):
    '''
    Let the value through if it is the same that was last seen. If
    ``predicate`` is provided it will be used to assess if a value is distinct
    from previous values
    '''

    def __init__(self, predicate=None):
        self._fn = predicate if predicate is not None else lambda x, y: x == y
        self._last = {}

    def _operate(self, val, sid):
        if sid in self._last:
            if self._fn(val, self._last[sid]):
                raise ObservableStopError()

        self._last[sid] = val  # distinct, store
        return val


class Do_Action_Operator(ObservableOperator):
    '''
    It forwards values after calling ``action`` with the incoming value.

    The value is forwarded as is, regardless of the actions of ``action``
    '''

    def __init__(self, action):
        self._action = action

    def _operate(self, val, sid):
        self._action(val)
        return val


class Filter_Operator(ObservableOperator):
    '''
    Forward the value only if ``predicate`` evaluates the value to ``True``
    '''

    def __init__(self, predicate):
        self._fn = predicate

    def _operate(self, val, sid):
        if self._fn(val):
            return val

        raise ObservableStopError()  # won't deliver


class First_Operator(ObservableOperator):
    '''
    Take only the first value.

    If ``predicate`` is provided, it will be used to determine which is the
    *first* value.
    '''

    def __init__(self, predicate=None):
        self._fn = predicate if predicate is not None else lambda x: True

    def on_next(self, val, sid):
        if self._fn(val):
            self._next(val, sid)
            self.on_completed(sid)


class First_Or_Default_Operator(ObservableOperator):
    '''
    Take only the 1st value. If ``predicate`` is provided, it will be invoked
    with each value and the 1st value will be that for which ``predicate``
    returns ``True``

    If no *first* value can be delivered, the operator will deliver
    ``default_value`` on completion.
    '''

    def __init__(self, predicate=None, default_value=None):
        self._delivered = False
        self._fn = predicate if predicate is not None else lambda x: True
        self._default = default_value

    def on_next(self, val, sid):
        if self._fn(val):
            self.on_completed(sid)

    def on_completed(sid):
        if not self._delivered:
            self._delivered = True
            self._next(self._default, sid)
            super().on_completed(sid)


class Map_Operator(ObservableOperator):
    '''
    Apply ``fn`` to the generated values generated, forwarding each result
    '''
    def __init__(self, fn):
        self.fn = fn

    def _operate(self, val, sid):
        return self.fn(val)


class Nop_Operator(ObservableOperator):
    '''
    A no-operation operator. It will simply forward values.
    '''
    pass  # forwards all operations


class Publish_Operator(ObservableOperator):
    '''
    Freezes an observable source to make it multicast. Several subscriptions
    can take place without values being forwarded to them.

    It supports the following additional operations:

      - ``auto_connect(count)``

        Unfreeze the observable after ``count`` subscriptions have taken place

      - ``connect()``

        Unfreeze the observable
    '''
    _cold = -1

    def __init__(self):
        self._sid = super()._get_sid()

    def _get_sid(self):
        return self._sid

    def _substrigger(self, sid):
        if self._cold <= 0:  # connected (0) or awaiting connect (-1)
            return Disposable(sid=sid)

        if self._cold == 1:  # last auto-connection
            disp = super()._substrigger(sid)  # trigger
        else:
            disp = Disposable(sid=sid)

        self._cold -= 1
        return disp

    def auto_connect(self, count):
        # if 0 were allowed, it wouldn't subscribe later
        self._cold = max(count, 1)
        return self

    def connect(self):
        self._cold = 1  # will be removed in _substrigger
        self._substrigger(self._sid)
        return self

    def on_completed(self, sid):
        self._cold = 1  # wait for 1 subs to re-subscribe
        super().on_completed(sid)


class Switch_Map_Operator(ObservableOperator):
    '''
    Swith to another observable based:

      - If ``handler`` is an *Observable*, it will be switched to it

      - Else, ``handler`` must be a callable accepting the value and returning
        the value to switch to

        - If the return value is an *Observable* it will be the result to
          switch to

        - Else, the return value will be converted using ``Observable.of``
    '''

    def __init__(self, handler):
        if isinstance(handler, Observable):
            self._obsgen = lambda x: handler
        else:
            self._obsgen = lambda x: handler(x)

        self._sidmap = {}  # new observable to old subs id (sid)
        self._sidobs = {}  # obs, id for new subs
        self._obs = None

    def on_next(self, val, sid):
        if sid in self._sidmap:  # comes from new observer
            orig_sid = self._sidmap[sid]
            if orig_sid is None:  # late value, purge
                self._sidmap.pop(orig_sid)
            else:
                self._next(val, orig_sid)
            return

        # A native sid has arrived
        if sid in self._sidobs:
            obs, obs_sid = self._sidobs.pop(sid)
            obs._unsubscribe(obs_sid)
            self._sidmap[obs_sid] = None  # mark for purging

        obs = self._obsgen(val)
        if not isinstance(obs, Observable):
            obs = Observable.of(obs)

        disp = obs.subscribe(self)
        obs_sid = disp.sid

        self._sidobs[sid] = obs, obs_sid
        self._sidmap[obs_sid] = sid


class Take_Operator(ObservableOperator):
    '''
    Forward at most ``count`` values before ending the subscription.
    '''

    # _timed_op = True

    def __init__(self, count):
        self.to_take = defaultdict(lambda: int(count))

    def on_next(self, val, sid):
        totake = self.to_take[sid]
        if totake:
            self.to_take[sid] = totake - 1
            # self._delay_next(sid, lambda: self._next(val, sid))
            self._next(val, sid)

        if totake <= 1:
            self.to_take[sid] = 0
            self.on_completed(sid)
            self._unsubscribe(sid)


class Throw__Operator(ObservableOperator):
    '''
    Generate an error ``throw`` as the error value

    .. note:: This is the operator and not the ``throw_`` source.
    '''

    def __init__(self, throw):
        self._throw = throw

    def on_next(self, val, sid):
        self.on_error(self._throw, sid)
