import cast from '../internal/cast'
import assert from '../internal/assert'
import checkUnique from '../internal/checkUnique'

import StateWithSideEffects from './StateWithSideEffects'
import {state} from './StateWithSideEffects'


function _bindActionFunctionToAppDispatcher(actionFunction) {

  return AppDispatcher =>
    (...args) => AppDispatcher.emit({...actionFunction(...args)})
}

/**
 * Takes a map of ActionFunctions indexed by ActionType and binds each to the
 * AppDispatcher. That is, when a bound function is called, it automatically
 * dispatches its message to the channel.
 *
 * **Note:**
 * 1. This method is actually a higher order function. It returns a function
 *    that accepts the AppDispatcher object as a parameter. This way, the
 *    AppDispatcher is not hard-coded dependency.
 *
 * @param {Map<string,boolean>} ActionTypes
 * @param {Map<ActionType,Function>} ActionFunctions
 * @returns {Function} a function that binds the action functions to the app dispatcher
 * @private
 */
export function bindActionFunctions(ActionTypes, ActionFunctions) {

  return AppDispatcher =>

    Object.keys(ActionTypes).reduce(
      (channelActions, action) => ({
        ...channelActions,
        [action]: _bindActionFunctionToAppDispatcher(ActionFunctions[action])(AppDispatcher)
      }),
      {}
    )
}

/**
 * @deprecated You rarely (really) need a *pre-bound* selector (emphasis:
 * "pre-bound"). For this reason, these are deprecated.
 *
 * Takes a map of ActionObservables *not necessarily indexed by ActionType* and binds each
 * to the ChannelStateObservable. The ChannelStateObservable is the channel's state,
 * wrapped in a Kefir stream (otherwise known as an *observable*).
 *
 * Since the ChannelStateObservable represents the state, an ActionObservable is a
 * way of observing (aka "selecting") arbitrary parts of the state tree.
 *
 * **Note:**
 * 1. This method is actually a higher order function. It returns a function
 *    that accepts a ChannelStateObservable object as a parameter. This way, the
 *    ChannelStateObservable is not hard-coded dependency.
 *
 * TODO global rename ActionObservable => SelectionObservable
 *
 * @param {Map<string,Observable>} ActionObservables
 * @returns {Function} a function that binds the action observables to the channel observable
 * @private
 */
function _bindActionObservables(ActionObservables) {

  return channelObservable =>

    Object.keys(ActionObservables).reduce(
      (total, observable) => Object.assign(
        total,
        {[observable]: ActionObservables[observable](channelObservable)}
      ),
      {}
    )
}

/**
 * Creates the channel's state observable using the given channel name.
 *
 * When an action comes in, it will call the corresponding reducer with the payload,
 * and pass the new state to the observable.
 *
 * Every reducer is called with these parameters:
 * 1. the current state
 * 2. the action payload
 * 3. a `endOfSideEffects` function that can be used to report the end of all the
 * side effects.
 *
 * In addition to updating the state, every reducer can also dispatch side
 * effects---which are just messages that are handled by other reducers or sagas. By
 * using the result of the `endOfSideEffects` function as the last side effect, it is
 * possible to tell when the entire reducer workflow completes... or so that's the
 * idea.
 *
 * Each reducer has this signature:
 *
 * ```
 * (state:ChannelState, payload:Payload, endOfSideEffects:Payload => Message)
 * => StateWithSideEffects
 * ```
 *
 * TODO sideEffectResult may not actually fire correctly, specially when side effects
 * are handled by async sagas.
 *
 * **Notes:**
 * 1. Every ActionType must have a corresponding Reducer.
 * 2. This method is actually a higher order function. It returns a function
 *    that accepts an AppDispatcher object as a parameter. This way, the
 *    AppDispatcher is not hard-coded dependency.
 *
 * @param {string} channel
 * @param {Map<ActionType,Function>} Reducers
 * @returns {Function} a function that creates the channel's state observable.
 * @private
 */
function _createChannelStateObservable(channel, Reducers) {

  return AppDispatcher =>

    AppDispatcher
      .filter(x => x && x.channel === channel)
      .scan(
        (stateWithSideEffects, action) => {

          const reducer = Reducers[action.actionType]

          if (!reducer) {
            throw new Error(`Channel ${channel} does not support ${action.actionType}`)
          }

          const endOfSideEffects = payload => ({
            channel: `${channel}Result`,
            actionType: `${action.actionType}Result`,
            payload
          })

always return a StateWithSideEffects (code hardening)

          return cast(
            reducer(stateWithSideEffects.state, action.payload, endOfSideEffects),
            StateWithSideEffects
          )
        },
        state(Reducers.initialState || {})
        )
}


/**
 * The idea is that you can use these observables to observe the end of a reducer +
 * side effects.
 * @param {string} channel
 * @param {Map<string,*>} ActionTypes
 * @returns {Function} function that binds AppDispatcher to the observables
 * @private
 */
function _createEndOfActionsObservables(channel, ActionTypes) {

  return AppDispatcher =>

    Object.keys(ActionTypes).reduce(
      (observables, action) => Object.assign(
        observables,
        {
          [`${action}ResultObservable`]:
            AppDispatcher
              .filter(x =>
              x.channel === `${channel}Result` && x.actionType === `${action}Result`)
              .map(x => x.payload)
        }
      ),
      {}
    )
}

/* eslint-disable no-console */
/**
 * The channel consists of
 * - the channel name
 * - the state observable
 * - bound (aka "live") action functions
 * - bound state selectors (which will probably be deprecated in a future release)
 *
 * It is created from a map of the ActionTypes. Each ActionType has a corresponding
 * reducer, which handles incoming messages. Each ActionType also has a corresponding
 * ActionFunction that's used to dispatch messages.
 *
 * One catch is that the *names* of the ActionFunctions and the ActionObservables must
 * be globally unique. This isn't hard to achieve as long as you:
 *
 * 1. use the channel name in the action/observable. Ex: createDoc
 * 2. use the word "observable" in the observables. Ex: docObservable
 *
 * @param {Object} opts
 * @param {string} opts.channel
 * @param {Map<string,*>} opts.ActionTypes - map of action type constants
 * @param {Map<ActionType,Function>} opts.Reducers - map of reducers, indexed by
 * ActionType. Additionally, reducers have an `initialState` property.
 * @param {Map<ActionType,Function>} opts.ActionFunctions - map of action functions,
 * indexed by ActionType
 * @param {Map<string,Function>} opts.ActionObservables (optional) - higher order
 * functions that take the ChannelStateObservable as input and return an observable that
 * selects parts of the state tree. **This will probably be deprecated.**
 * @returns {Function} that binds the channel to the app dispatcher
 * @private
 */
export function _createChannel(
  {channel, ActionTypes, Reducers, ActionFunctions, ActionObservables}) {

  ActionObservables = ActionObservables || {}

  assert(typeof channel === 'string', 'Needs a channel and it needs to be a string')
  assert(ActionTypes, 'Need ActionTypes')
  assert(Reducers, 'Need Reducers')
  assert(ActionFunctions, 'Need action functions')

every action must have an action function and a reducer

  Object.keys(ActionTypes).forEach(action => {
    assert(
      ActionFunctions[action],
      `Channel ${channel} is missing action function "${action}"`
    )
    assert(Reducers[action], `Channel ${channel} is missing reducer "${action}"`)
  })

need an initial state; otherwise defaults to {}

  if (!Reducers.initialState) {
    console.warn(`Channel ${channel} doesn't have initialState`)
  }

  return ({AppDispatcher}) => {

    const stateWithSideEffectsObservable =
      _createChannelStateObservable(channel, Reducers)(AppDispatcher)
    const stateObservable = stateWithSideEffectsObservable.map(x => x.state)

    return {
      name: channel,
      stateWithSideEffectsObservable,
      actions: {
        ...bindActionFunctions(ActionTypes, ActionFunctions)(AppDispatcher),
      },
      observable: {
        [channel]: stateObservable,
      },
      channel: {

@deprecated

        ..._bindActionObservables(ActionObservables)(stateObservable),

@deprecated

        ..._createEndOfActionsObservables(channel, ActionTypes)(AppDispatcher),
      }
    }
  }
}


export default function createChannels({rawChannels, ...args}) {
  checkUnique(rawChannels, 'channel', 'Cannot have two channels with the same name');
  return rawChannels.map(s => _createChannel(s)({...args}))
}
h