"""
Redux design pattern
--------------------

As applications grow the number of components
vying to keep their state synchronised becomes unwieldy.
A better way to manage state is centralise it
so that there is a single source of truth. The
consequence of which is easy to replay, serialise
and rehydrate applications.

For further reading, check out the redux.js docs

* https://redux.js.org/introduction/motivation

It also enables an easily unit testable system
since state updates are pure function with easy to
separate single responsibility components. Middleware
components are plug and play, drop one in to add
behaviour to your app without fear of altering
existing behaviour.

.. autoclass:: Store
   :members:

.. autofunction:: combine_reducers

"""
import copy
import queue
from functools import wraps
from forest.observe import Observable
from forest.export import export
from typing import Callable, Iterable


# Type aliases
Action = dict
State = dict
Reducer = Callable[[State, Action], State]
Middleware = Callable[["Store", Action], Iterable[Action]]


__all__ = []


@export
def combine_reducers(*reducers):
    """Simple combine passes action and state to all reducers

    :returns: reducer function
    """

    def wrapped(state, action):
        state = copy.deepcopy(state)
        for reducer in reducers:
            state = reducer(state, action)
        return state

    return wrapped


@export
class Store(Observable):
    """Observable state container

    The redux design pattern is a simple way to
    keep track of state changes. A reducer combines an action with
    the current state to produce the next state. The reducer
    should be a pure function in the sense of not having side effects.

    Non-pure behaviour can be incorporated through the use of
    middleware. Middleware takes an action and either passes it on,
    filters it, enriches it or emits new actions.

    The store is an observable that emits states, views can register
    themselves with the store to receive the latest states as and
    when they are created.

    >>> store.add_subscriber(listener)

    :param reducer: function combines action and state to produce new state
    :param initial_state: optional initial state, default {}
    :param middlewares: list of middleware functions that intercept actions
    """

    def __init__(self, reducer, initial_state=None, middlewares=()):
        self.reducer = reducer
        self.state = initial_state if initial_state is not None else {}
        self.middlewares = middlewares
        self.in_progress = False
        self.queue = queue.Queue()
        super().__init__()

    def dispatch(self, action):
        """Apply reducer and notify listeners of new state

        :param action: plain dict consumed by the reducer
        """
        if self.in_progress:
            # Add asynchronous action to backlog
            self.queue.put(action)
            return

        # Synchronous processing
        self.sync_process(action)

        # Process backlog
        while not self.queue.empty():
            next_action = self.queue.get()
            self.sync_process(next_action)
            self.queue.task_done()

    def sync_process(self, action):
        """Pass action through middleware/reducer pipeline"""
        actions = self.pure(action)
        for middleware in self.middlewares:
            actions = self.bind(middleware, self, actions)
        for _action in actions:
            self.state = self.reducer(self.state, _action)
            self.in_progress = True
            self.notify(self.state)
            self.in_progress = False

    @staticmethod
    def pure(action):
        """Embed action into action generator"""
        yield action

    @staticmethod
    def bind(middleware, store, actions):
        """Flat map action generators from middleware into action generator"""
        for action in actions:
            yield from middleware(store, action)
