"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.makeFSM = makeFSM;

var _ramda = require("ramda");

var _utils = require("../../utils");

var _types = require("./types");

var _rx = require("rx");

var Rx = _interopRequireWildcard(_rx);

var _fastJsonPatch = require("fast-json-patch");

var jsonpatch = _interopRequireWildcard(_fastJsonPatch);

var _properties = require("../properties");

var _utils2 = require("./utils");

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } // Patch library : https://github.com/Starcounter-Jack/JSON-Patch


// NOTE1 : dont use observe functionality for generating patches
// it uses JSON stringify which makes it impossible to have functions in the
// model object
// NOTE2 : patches are applied IN PLACE

/**
 * @typedef {String} EventName
 */
/**
 * @typedef {SinkName} DriverName
 */
/**
 * @typedef {*} EventData
 */
/**
 * @typedef {String} State
 */
/**
 * @typedef {String} JSON_Pointer
 * albeit a string with a particular format
 * cf. https://tools.ietf.org/html/rfc6901
 * The ABNF syntax of a JSON Pointer is:
 *  json-pointer    = *( "/" reference-token )
 *  reference-token = *( unescaped / escaped )
 *  unescaped       = %x00-2E / %x30-7D / %x7F-10FFFF ; %x2F ('/') and %x7E ('~') are excluded from
 *   'unescaped' escaped         = "~" ( "0" / "1" ) ; representing '~' and '/', respectively
 */
/**
 * @typedef {String} TransitionName
 */
/**
 * @typedef {Object.<EventName, Event>} Events
 */
/**
 * @typedef {function(Sources):EventData} Event
 */
/**
 * @typedef {*} FSM_Model
 */
/**
 * @typedef {*} Command
 */
/**
 * @typedef {*} Payload
 */
/**
 * @typedef {*} ActionResponse
 */
/**
 * @typedef {String} ZeroDriver
 */
/**
 * @typedef {{command : Command, payload : Payload}} Request
 */
/**
 * @typedef {function(FSM_Model, EventData):Request} RequestFn
 */
/**
 * @typedef {{driver : SinkName|ZeroDriver, request : RequestFn, }} ActionRequest
 */
/**
 * @typedef {function(FSM_Model, EventData) : Boolean} EventGuard
 */
/**
 * @typedef {function(ActionResponse) : Boolean} ActionGuard
 */
/**
 * @typedef {{op : "add", path : JSON_Pointer, value : *}} Op_Add
 */
/**
 * @typedef {{op : "replace", path : JSON_Pointer, value : *}} Op_Replace
 */
/**
 * @typedef {{op : "remove", path : JSON_Pointer}} Op_Remove
 */
/**
 * @typedef {{op : "move", from : JSON_Pointer, path : JSON_Pointer}} Op_Move
 */
/**
 * @typedef {{op : "copy", from : JSON_Pointer, path : JSON_Pointer}} Op_Copy
 */
/**
 * @typedef {{op : "copy", path : JSON_Pointer, value : *}} Op_Test
 */
/**
 * @typedef {Op_Add|Op_Remove|Op_Replace|Op_Move|Op_Copy|Op_Test} JSON_Patch
 */
/**
 * @typedef {JSON_Patch} UpdateOperation
 */
/**
 * @typedef {function(FSM_Model, EventData, ActionResponse) : UpdateOperation[]} UpdateFn
 */
/**
 * @typedef {{action_guard : ActionGuard, target_state : State, model_update : UpdateFn}} TransEval
 */
/**
 * @typedef {{event_guard : EventGuard, re_entry: Boolean, action_request : ActionRequest,
 *   transition_evaluation : TransEval[]}} Transition
 */
/**
 * @typedef {{origin_state : State, event : EventName, target_states : Transition[]}}
 *   TransitionOptions
 */
/**
 * @typedef {Object.<TransitionName, TransitionOptions>} Transitions
 */
/**
 * @typedef {String} AWAITING_EVENTS
 */
/**
 * @typedef {String} AWAITING_ACTION_RESPONSE
 */
/**
 * @typedef {AWAITING_EVENTS|AWAITING_ACTION_RESPONSE} InternalState
 */
/**
 * @typedef {{internal_state : InternalState, external_state : State, model : FSM_Model,
 *   current_event_name : EventName | Null, current_event_data : EventData | Null,
 *   current_event_guard_index : Number | Null, current_action_request_driver : DriverName | Null,
 *   sinks : Sinks | Null}} FSM_State
 */
/**
 * @typedef {String} UserEventPrefix
 */
/**
 * @typedef {String} DriverEventPrefix
 */
/**
 * @typedef {Object.<EventName, EventData>} LabelledUserEvent
 */
/**
 * @typedef {Object.<DriverName, ActionResponse>} LabelledDriverEvent
 */
/**
 * @typedef {Object.<UserEventPrefix, LabelledUserEvent>} UserEvent
 */
/**
 * @typedef {Object.<DriverEventPrefix, LabelledDriverEvent>} DriverEvent
 */

var $ = Rx.Observable;

function removeZeroDriver(driverNameArray) {
  return (0, _ramda.filter)(function removeZero(driverName) {
    return driverName != _properties.ZERO_DRIVER;
  }, driverNameArray);
}

function prefixWith(prefix) {
  return function _prefixWith(obj) {
    return _defineProperty({}, prefix, obj);
  };
}

/**
 * @param {Object.<string, *>} fsmEvent
 * @returns {String}
 */
function getPrefix(fsmEvent) {
  return (0, _ramda.keys)(fsmEvent)[0];
}

/**
 * NOTE : fsmEvent MUST only have one key
 * @param fsmEvent
 * @returns {UserEventPrefix|DriverEventPrefix}
 */
function getEventOrigin(fsmEvent) {
  return getPrefix(fsmEvent);
}

/**
 * NOTE : fsmEvent MUST only have one key
 * @param prefix
 * @param {UserEvent | DriverEvent} fsmEvent
 * @returns {LabelledUserEvent | LabelledDriverEvent}
 */
function getFsmEventValue(prefix, fsmEvent) {
  return fsmEvent[prefix];
}

/**
 *
 * @param {String} eventOrDriverName
 * @param {LabelledUserEvent|LabelledDriverEvent} fsmEventValue
 * @returns {EventData | ActionResponse}
 */
function getEventDataOrActionResponse(eventOrDriverName, fsmEventValue) {
  return fsmEventValue[eventOrDriverName];
}

/**
 *
 * @param fsmEvent
 * @returns {{fsmEventOrigin: (UserEventPrefix|DriverEventPrefix), fsmEventValue:
 *   (LabelledUserEvent|LabelledDriverEvent)}}
 */
function destructureFsmEvent(fsmEvent) {
  var prefix = getEventOrigin(fsmEvent);
  var fsmEventValue = getFsmEventValue(prefix, fsmEvent);

  return {
    fsmEventOrigin: prefix,
    fsmEventValue: fsmEventValue
  };
}

/**
 *
 * @param fsmEventValue
 * @returns {{eventOrDriverName: String, eventDataOrActionResponse: (EventData|ActionResponse)}}
 */
function destructureFsmEventValue(fsmEventValue) {
  var eventOrDriverName = getPrefix(fsmEventValue);
  var eventDataOrActionResponse = getEventDataOrActionResponse(eventOrDriverName, fsmEventValue);

  return {
    eventOrDriverName: eventOrDriverName,
    eventDataOrActionResponse: eventDataOrActionResponse
  };
}

/**
 *
 * @param {Transitions} transitions
 * @returns {Object.<State, EventName[]>}
 */
function computeStateEventMap(transitions) {
  return (0, _ramda.reduce)(function ( /*OUT*/accStateEventMap, transName) {
    var transOptions = transitions[transName];
    var origin_state = transOptions.origin_state,
        event = transOptions.event;

    accStateEventMap[origin_state] = accStateEventMap[origin_state] || [];
    accStateEventMap[origin_state].push(event);

    return accStateEventMap;
  }, {}, (0, _ramda.keys)(transitions));
}

/**
 *
 * @param {Transitions} transitions
 * @returns {Object.<State, Object.<EventName, TransitionName>>}
 */
function computeStateEventToTransitionNameMap(transitions) {
  return (0, _ramda.reduce)(function ( /*OUT*/acc, transName) {
    var transOptions = transitions[transName];
    var origin_state = transOptions.origin_state,
        event = transOptions.event;

    acc[origin_state] = acc[origin_state] || {};
    acc[origin_state][event] = transName;

    return acc;
  }, {}, (0, _ramda.keys)(transitions));
}

/**
 * Returns the action request corresponding to the first guard satisfied, as
 * defined by the order of the target_states array
 * @param {Transitions} transitions
 * @param {String} transName
 * @param {FSM_Model} model
 * @param {EventData} eventData
 * @return {{ actionRequest : ActionRequest | Null, transitionEvaluation, satisfiedGuardIndex :
 *   Number | Null, reEntry: Boolean, noGuardSatisfied : Boolean}}
 */
function computeTransition(transitions, transName, model, eventData) {
  var NOT_FOUND = -1;
  var targetStates = transitions[transName].target_states;

  var satisfiedGuardIndex = (0, _ramda.findIndex)(function (transition) {
    /** @type {EventGuard} */
    var eventGuard = transition.event_guard;

    if (eventGuard == _properties.EV_GUARD_NONE) {
      return true;
    } else {
      // EventGuard :: Model -> EventData -> Boolean
      var wrappedEventGuard = (0, _ramda.tryCatch)(eventGuard, (0, _utils.handleError)(_properties.CONTRACT_EVENT_GUARD_CANNOT_FAIL));
      var guardValue = wrappedEventGuard(model, eventData);
      (0, _utils.assertContract)(_utils.isBoolean, [guardValue], "computeTransition: " + _properties.CONTRACT_EVENT_GUARD_FN_RETURN_VALUE);

      return guardValue;
    }
  }, targetStates);

  return satisfiedGuardIndex !== NOT_FOUND ? {
    satisfiedGuardIndex: satisfiedGuardIndex,
    actionRequest: targetStates[satisfiedGuardIndex].action_request,
    reEntry: targetStates[satisfiedGuardIndex].re_entry,
    transitionEvaluation: targetStates[satisfiedGuardIndex].transition_evaluation,
    noGuardSatisfied: false
  } : {
    satisfiedGuardIndex: null,
    actionRequest: null,
    reEntry: null,
    transitionEvaluation: null,
    noGuardSatisfied: true
  };
}

function isZeroActionRequest(actionRequest) {
  return actionRequest == _properties.ACTION_REQUEST_NONE || isZeroDriver(actionRequest.driver);
}

function isZeroDriver(driver) {
  return driver == _properties.ZERO_DRIVER;
}

/**
 *
 * @param {FSM_Model} model
 * @param {UpdateOperation[]} modelUpdateOperations
 * @returns {FSM_Model}
 */
function applyUpdateOperations( /*OUT*/model, modelUpdateOperations) {
  (0, _utils.assertContract)(_types.isArrayUpdateOperations, [modelUpdateOperations], "applyUpdateOperations : " + _properties.CONTRACT_MODEL_UPDATE_FN_RETURN_VALUE);

  jsonpatch.apply(model, modelUpdateOperations);
  return model;
}

/**
 *
 * @param sources
 * @param settings
 * @param {Event} event$Fn Event factory function
 * @param {EventName} eventName
 * @returns {Observable}
 * @throws
 */
function _labelEvents(sources, settings, event$Fn, eventName, _) {
  var event$Fn$ = event$Fn(sources, settings);
  (0, _utils.assertContract)(_types.checkIsObservable, [event$Fn$], "event factory function for event " + eventName + " must return an observable!");

  return event$Fn$.map(prefixWith(eventName));
}
var computeAndLabelEvents = (0, _ramda.curry)(_labelEvents);

function getDriverNames(transOptions, transName) {
  var target_states = transOptions.target_states;
  /** @type {Array.<String|ZeroDriver>} */

  var driverNames = (0, _ramda.map)(function (transition) {
    var action_request = transition.action_request;

    var _ref2 = action_request || {},
        driver = _ref2.driver;

    return driver;
  }, target_states);

  return driverNames;
}

function setFsmStateSinksToNull(fsmState) {
  var internal_state = fsmState.internal_state,
      external_state = fsmState.external_state,
      model = fsmState.model,
      clonedModel = fsmState.clonedModel,
      current_event_name = fsmState.current_event_name,
      current_event_data = fsmState.current_event_data,
      current_event_guard_index = fsmState.current_event_guard_index,
      current_action_request_driver = fsmState.current_action_request_driver,
      current_action_request = fsmState.current_action_request,
      sinks = fsmState.sinks;


  return {
    sinks: null,
    internal_state: internal_state, external_state: external_state, model: model, clonedModel: clonedModel,
    current_event_name: current_event_name, current_event_data: current_event_data, current_event_guard_index: current_event_guard_index,
    current_action_request_driver: current_action_request_driver, current_action_request: current_action_request
  };
}

/**
 * Returns the action request corresponding to the first guard satisfied, as
 * defined by the order of the target_states array
 * @param {Transitions} transitions
 * @param {String} transName
 * @param {Number} current_event_guard_index
 * @param model
 * @param {ActionResponse} actionResponse
 * @return {{target_state: null, re_entry: boolean, model_update: null, noGuardSatisfied: boolean}}
 */
function evaluateTransitionWhenActionResponse(transitions, transName, current_event_guard_index, model, actionResponse) {
  /** @type {Transition} */
  var transition = transitions[transName].target_states[current_event_guard_index];
  /** @type {Array} */
  var actionResponseGuards = transition.transition_evaluation;
  var reEntry = transition.re_entry;

  var foundSatisfiedGuard = (0, _ramda.find)(function (transEval) {
    var action_guard = transEval.action_guard,
        target_state = transEval.target_state,
        model_update = transEval.model_update;


    if (action_guard == _properties.AR_GUARD_NONE) {
      // if no action guard is configured, it is equivalent to a passing guard
      return true;
    } else {
      // ActionGuard :: ActionResponse -> Boolean
      var wrappedActionGuard = (0, _ramda.tryCatch)(action_guard, (0, _utils.handleError)(_properties.CONTRACT_ACTION_GUARD_CANNOT_FAIL));
      var guardValue = wrappedActionGuard(model, actionResponse);
      (0, _utils.assertContract)(_utils.isBoolean, [guardValue], "computeTransition: " + _properties.CONTRACT_ACTION_GUARD_FN_RETURN_VALUE);

      return guardValue;
    }
  }, actionResponseGuards);

  return foundSatisfiedGuard ? {
    target_state: foundSatisfiedGuard.target_state,
    model_update: foundSatisfiedGuard.model_update,
    re_entry: reEntry
  } : { target_state: null, re_entry: null, model_update: null, noGuardSatisfied: true };
}

function performTransitionWhenNoActionRequest(reEntry, entryComponents, external_state, model, clonedModel, eventData, transitionEvaluation, sources, settings) {
  // TODO : check contract : when no action requests, only ONE action_guard which MUST be Zero
  var _transitionEvaluation = transitionEvaluation[0],
      target_state = _transitionEvaluation.target_state,
      model_update = _transitionEvaluation.model_update;

  var wrappedModelUpdate = (0, _ramda.tryCatch)(model_update, (0, _utils.handleError)(_properties.CONTRACT_MODEL_UPDATE_FN_CANNOT_FAIL));
  var modelUpdateOperations = wrappedModelUpdate(clonedModel, eventData, null, settings);
  var entryComponent = entryComponents[target_state];

  var newSinks = void 0;

  // Set values for next FSM state update
  var newModel = applyUpdateOperations(model, modelUpdateOperations);
  // NOTE: could also be applyUpdateOperations(clonedModel,...) dont know which
  // is faster
  var newClonedModel = (0, _ramda.clone)(newModel);
  // NOTE : The model to be passed to the entry component is post update
  // NOTE2 : {} because we still want to terminate, i.e. LEAVE previous state. This would be
  // done with the switchMap. Reminder : null means filter i.e. do nothing
  var stateEntryComponent = entryComponent ? entryComponent(newClonedModel) : (0, _ramda.always)({});
  (0, _utils.assertContract)((0, _ramda.either)(_ramda.isNil, _types.checkStateEntryComponentFnMustReturnComponent), [stateEntryComponent], "state entry component function " + entryComponent.name + " \n                    for state " + target_state + " MUST return a component or be null");

  if (reEntry && target_state === external_state || target_state !== external_state) {
    // When reentry flag is set, and target state is the origin state, then re-enter that state by
    // re-executing the state entry action
    newSinks = stateEntryComponent(sources, settings);
  } else {
    // When no reentry, dont do no action
    newSinks = null;
  }

  return {
    external_state: target_state,
    model: newModel,
    clonedModel: newClonedModel,
    sinks: newSinks,
    internal_state: _properties.AWAITING_EVENTS,
    current_event_guard_index: null,
    current_event_name: null,
    current_event_data: null,
    current_action_request_driver: null,
    current_action_request: null
  };
}

function performTransitionWhenActionResponse(fsmState, transition, fsmCompiled, actionResponse, sources, settings) {
  var target_state = transition.target_state,
      re_entry = transition.re_entry,
      model_update = transition.model_update;
  var external_state = fsmState.external_state,
      model = fsmState.model,
      clonedModel = fsmState.clonedModel,
      current_event_data = fsmState.current_event_data;
  var entryComponents = fsmCompiled.entryComponents;

  var newSinks = void 0;

  var wrappedModelUpdate = (0, _ramda.tryCatch)(model_update, (0, _utils.handleError)(_properties.CONTRACT_MODEL_UPDATE_FN_CANNOT_FAIL));
  var modelUpdateOperations = wrappedModelUpdate(model, current_event_data, actionResponse, settings);

  var entryComponentFactory = entryComponents[target_state];
  (0, _utils.assertContract)(_types.isEntryComponentFactory, [entryComponentFactory], "Error while trying to get entry component factory for state " + target_state + " : \n            component configured in fsm must be null or a function!");

  // Set new model's values for next FSM state update
  var newModel = applyUpdateOperations( /*OUT*/model, modelUpdateOperations);
  var newClonedModel = (0, _ramda.clone)(newModel);
  var entryComponent = entryComponentFactory ? entryComponentFactory(newClonedModel) : null;
  (0, _utils.assertContract)(_types.isEntryComponent, [entryComponent], "Error while trying to get entry component for state " + target_state + " : \n            configured factory must return null or a component function :: sources -> settings -> Sinks!");

  if (re_entry && target_state === external_state || target_state !== external_state) {
    // When reentry flag is set, and target state is the origin state, then re-enter that
    // state by re-executing the state entry action
    newSinks = entryComponent ? entryComponent(sources, settings) : {};
  } else {
    // we re-enter the same state, but re-entry flag is not set -> dont do nothing
    newSinks = null;
  }

  return {
    internal_state: _properties.AWAITING_EVENTS,
    external_state: target_state,
    // Note : The model to be passed to the entry component is post update
    model: newModel,
    clonedModel: newClonedModel,
    sinks: newSinks,
    current_event_guard_index: null,
    current_event_name: null,
    current_event_data: null,
    current_action_request_driver: null,
    current_action_request: null
  };
}

function processEventWhenAwaitingUserEvents(fsmCompiled, sources, settings, fsmState, fsmEvent) {
  // If received DriverEvent
  // -- Log warning, Ignore, no state modification, sinks = Null
  // Else If received UserEvent
  // -- If userEvent is NOT among the configured events for the FSM's external state
  // -- -- Log warning, Ignore, no state modification (could also queue??), sinks = Null
  // -- Else If no guards is passed :
  // -- -- no state modification (could also queue??), sinks = Null
  // -- -- Else a guard is passed, get the action request from it :
  // -- -- -- If ActionRequest is Zero
  // -- -- -- -- no need to wait for a response, change the fsm state directly
  // -- -- -- -- check contract : action_guard MUST be Zero
  // -- -- -- -- If re-entry YES && external_state == target_state:
  // -- -- -- -- -- sinks <- execute the component defined as entry for the state transitioned to
  // -- -- -- -- Else
  // -- -- -- -- -- sinks <- null, i.e. do nothing
  // -- -- -- -- internal_state <- AWAITING_EVENTS
  // -- -- -- -- current_event_data <- Null
  // -- -- -- -- current_action_request_driver <- Null
  // -- -- -- -- external_state <- target_state
  // -- -- -- -- model <- apply update operations
  // -- -- -- Else :
  // -- -- -- -- sinks <- Compute action request (MUST be non empty object)
  // -- -- -- -- internal_state <- AWAITING_RESPONSE
  // -- -- -- -- current_event_data <- event_data
  // -- -- -- -- current_action_request_driver <- the ONE key of sinks
  // -- -- -- -- external_state, model <- unmodified
  var transitions = fsmCompiled.transitions,
      entryComponents = fsmCompiled.entryComponents,
      stateEventsMap = fsmCompiled.stateEventsMap,
      stateEventToTransitionNameMap = fsmCompiled.stateEventToTransitionNameMap;

  // NOTE : We clone the model to eliminate possible bugs coming from user-defined functions
  // inadvertently modifying the model

  var external_state = fsmState.external_state,
      model = fsmState.model,
      clonedModel = fsmState.clonedModel;

  var newFsmState = void 0;

  // NOTE : fsmEvent only has one key by construction

  var _destructureFsmEvent = destructureFsmEvent(fsmEvent),
      fsmEventOrigin = _destructureFsmEvent.fsmEventOrigin,
      fsmEventValue = _destructureFsmEvent.fsmEventValue;

  var _destructureFsmEventV = destructureFsmEventValue(fsmEventValue),
      eventOrDriverName = _destructureFsmEventV.eventOrDriverName,
      eventDataOrActionResponse = _destructureFsmEventV.eventDataOrActionResponse;

  switch (fsmEventOrigin) {
    case _properties.DRIVER_PREFIX:
      console.warn('Received event from driver while awaiting user events! Ignoring...');
      newFsmState = setFsmStateSinksToNull(fsmState);
      break;

    case _properties.EVENT_PREFIX:
      /** @type {EventName[]} */
      var configuredEvents = stateEventsMap[external_state];
      var eventName = eventOrDriverName;
      /** @type {EventData} */
      var eventData = eventDataOrActionResponse;

      if (!configuredEvents || !(0, _ramda.find)((0, _ramda.equals)(eventName), configuredEvents)) {
        console.warn('Received event for which there is no transition defined!' + ' Ignoring...');
        newFsmState = setFsmStateSinksToNull(fsmState);
      } else {
        // Compute action request triggered by event, if any
        var transName = stateEventToTransitionNameMap[external_state][eventName];

        /** @type {ActionRequest | Null} */

        var _computeTransition = computeTransition(transitions, transName, clonedModel, eventData),
            actionRequest = _computeTransition.actionRequest,
            reEntry = _computeTransition.reEntry,
            transitionEvaluation = _computeTransition.transitionEvaluation,
            satisfiedGuardIndex = _computeTransition.satisfiedGuardIndex,
            noGuardSatisfied = _computeTransition.noGuardSatisfied;

        if (!!noGuardSatisfied) {
          // no guards is satisfied
          console.warn('Received event for which there is a transition defined but none' + ' of the defined guards were satisfied!' + ' Ignoring...');
          newFsmState = setFsmStateSinksToNull(fsmState);
        } else {
          if (isZeroActionRequest(actionRequest)) {
            newFsmState = performTransitionWhenNoActionRequest(reEntry, entryComponents, external_state, model, clonedModel, eventData, transitionEvaluation, sources, settings);
          } else {
            var _computeSinkFromActio = computeSinkFromActionRequest(actionRequest, model, eventData),
                request = _computeSinkFromActio.request,
                driver = _computeSinkFromActio.driver,
                sink = _computeSinkFromActio.sink;
            // model and external_state are unchanged


            newFsmState = {
              sinks: sink,
              internal_state: _properties.AWAITING_RESPONSE,
              current_event_guard_index: satisfiedGuardIndex,
              current_event_name: eventName,
              current_event_data: eventData,
              current_action_request_driver: driver,
              current_action_request: request,
              model: model, clonedModel: clonedModel, external_state: external_state
            };
          }
        }
      }
      break;

    default:
      throw 'evaluateEvent > case AWAITING_EVENTS : unknown fsmEventOrigin!';
  }

  return newFsmState;
}

function processEventWhenAwaitingResponseEvent(fsmCompiled, sources, settings, fsmState, fsmEvent) {
  // If received UserEvent
  // -- Log warning, Ignore, no state modification, sinks = Null
  // Else If received DriverEvent
  // -- If driverEvent is NOT from the expected driver (as to current_action_request_driver)
  // -- -- Log warning, Ignore, no state modification (could also queue??), sinks = Null
  // -- Else If action response fails all action guards :
  // -- -- Log Error, THROW, sinks = Null
  // -- -- Else action response pass the first action guard
  // -- -- -- external_state <- as defined by the transition for the successful action guard
  // -- -- -- sinks <- execute the component defined as entry for the state transitioned to
  // ?? with which sources and settings??
  // -- -- -- update operations <- compute model update
  // -- -- -- model <- apply update operations
  // -- -- -- internal_state <- AWAITING_EVENTS
  // -- -- -- current_event_data <- Null
  // -- -- -- current_action_request_driver <- Null
  // NOTE : We clone the model to eliminate possible bugs coming from user-defined functions
  // inadvertently modifying the model
  var events = fsmCompiled.events,
      transitions = fsmCompiled.transitions,
      entryComponents = fsmCompiled.entryComponents,
      stateEventsMap = fsmCompiled.stateEventsMap,
      stateEventToTransitionNameMap = fsmCompiled.stateEventToTransitionNameMap;
  var internal_state = fsmState.internal_state,
      external_state = fsmState.external_state,
      model = fsmState.model,
      clonedModel = fsmState.clonedModel,
      current_event_name = fsmState.current_event_name,
      current_event_data = fsmState.current_event_data,
      current_event_guard_index = fsmState.current_event_guard_index,
      current_action_request_driver = fsmState.current_action_request_driver,
      current_action_request = fsmState.current_action_request,
      sinks = fsmState.sinks;

  var newFsmState = void 0;

  // NOTE : fsmEvent only has one key by construction

  var _destructureFsmEvent2 = destructureFsmEvent(fsmEvent),
      fsmEventOrigin = _destructureFsmEvent2.fsmEventOrigin,
      fsmEventValue = _destructureFsmEvent2.fsmEventValue;

  var _destructureFsmEventV2 = destructureFsmEventValue(fsmEventValue),
      eventOrDriverName = _destructureFsmEventV2.eventOrDriverName,
      eventDataOrActionResponse = _destructureFsmEventV2.eventDataOrActionResponse;

  switch (fsmEventOrigin) {
    case _properties.EVENT_PREFIX:
      console.warn('Received event from user while awaiting driver\'s action response!' + ' Ignoring...');
      newFsmState = setFsmStateSinksToNull(fsmState);
      break;

    case _properties.DRIVER_PREFIX:
      var driverName = eventOrDriverName;
      var actionResponse = eventDataOrActionResponse;
      var request = actionResponse.request;

      var transName = stateEventToTransitionNameMap[external_state][current_event_name];

      if (driverName !== current_action_request_driver) {
        console.warn("\n              Received driver " + driverName + "'s event but not from the expected \n                 " + current_action_request_driver + " driver!\n\n                 Ignoring...");
        newFsmState = setFsmStateSinksToNull(fsmState);
      } else if (request != current_action_request || !(0, _ramda.equals)(request, current_action_request)) {
        console.warn("\n              Received action response through driver " + driverName + " and ignored it as that\n                 response does not match the request sent...");
        newFsmState = setFsmStateSinksToNull(fsmState);
        // TODO : document the fact that driver must return the request in the response
      } else {
        var evaluatedTransition = evaluateTransitionWhenActionResponse(transitions, transName, current_event_guard_index, model, actionResponse);

        if (evaluatedTransition.noGuardSatisfied) {
          console.error("While processing action response from driver " + driverName + ",\n                 executed all configured guards and none were satisfied! \n                  ' Throwing...");
          newFsmState = setFsmStateSinksToNull(fsmState);
          throw _properties.CONTRACT_SATISFIED_GUARD_PER_ACTION_RESPONSE;
        } else {
          newFsmState = performTransitionWhenActionResponse(fsmState, evaluatedTransition, fsmCompiled, actionResponse, sources, settings);
        }
      }
      break;

    default:
      throw "Received unexpected/unknown " + fsmEventOrigin + " event. \n            Can only process driver responses and user events!";
  }

  return newFsmState;
}

function makeFSM(_events, _transitions, _entryComponents, fsmSettings) {
  var fsmSignature = {
    events: _types.isFsmEvents,
    transitions: _types.isFsmTransitions,
    entryComponents: _types.isFsmEntryComponents,
    fsmSettings: _types.isFsmSettings
  };
  var fsmSignatureErrorMessages = {
    events: '',
    transitions: 'Invalid value for transitions parameter : must be non-empty object and must' + ' have at least one transition defined which involves the INIT event!',
    entryComponents: 'Invalid value for entryComponents parameter : must be non-empty object!',
    fsmSettings: "Invalid settings : some parameters are mandatory - check documentation!"
  };
  (0, _utils.assertContract)(_utils.checkSignature, [{ events: _events, transitions: _transitions, entryComponents: _entryComponents, fsmSettings: fsmSettings }, fsmSignature, fsmSignatureErrorMessages], '');
  (0, _utils.assertContract)(_types.checkTargetStatesDefinedInTransitionsMustBeMappedToComponent, arguments, 'makeFSM : Any target state which is referred to in the transitions parameter must be' + ' associated to a component via the entryComponents parameter!');
  (0, _utils.assertContract)(_types.checkOriginStatesDefinedInTransitionsMustBeMappedToComponent, arguments, 'makeFSM : Any origin state (except the initial state) which is referred to in the' + ' transitions parameter must be associated to a component via the entryComponents parameter!');
  (0, _utils.assertContract)(_types.checkEventDefinedInTransitionsMustBeMappedToEventFactory, arguments, 'makeFSM : Any event (except the initial event) which is referred to in the' + ' transitions parameter must be associated to a event factory function via the' + ' events parameter!');

  var init_event_data = fsmSettings.init_event_data,
      initial_model = fsmSettings.initial_model,
      sinkNames = fsmSettings.sinkNames,
      debug = fsmSettings.debug;

  // If debug, wrap functions to output log messages

  var _wrapIfDebug = wrapIfDebug({
    debug: debug,
    events: _events,
    transitions: _transitions,
    entryComponents: _entryComponents
  }),
      events = _wrapIfDebug.events,
      transitions = _wrapIfDebug.transitions,
      entryComponents = _wrapIfDebug.entryComponents;

  // 0.1 Pre-process the state machine configuration


  var stateEventsMap = computeStateEventMap(transitions);
  var stateEventToTransitionNameMap = computeStateEventToTransitionNameMap(transitions);

  /**
   *
   * @param {FSM_State} fsmState
   * @param {UserEvent | DriverEvent} fsmEvent
   * @param events
   * @param transitions
   * @param entryComponents
   * @param sources
   * @param settings
   * @returns {FSM_State}
   */
  function _evaluateEvent(events, transitions, entryComponents, sources, settings,
  /* OUT */fsmState, fsmEvent) {
    var newFsmState = void 0;
    var fsmCompiled = {
      events: events,
      transitions: transitions,
      entryComponents: entryComponents,
      stateEventsMap: stateEventsMap,
      stateEventToTransitionNameMap: stateEventToTransitionNameMap
    };
    var internal_state = fsmState.internal_state;


    switch (internal_state) {
      case _properties.AWAITING_EVENTS:
        newFsmState = processEventWhenAwaitingUserEvents(fsmCompiled, sources, settings, fsmState, fsmEvent);
        break;

      case _properties.AWAITING_RESPONSE:
        newFsmState = processEventWhenAwaitingResponseEvent(fsmCompiled, sources, settings, fsmState, fsmEvent);
        break;

      default:
        var err = 'Unexpected internal state reached by state machine !';
        console.error(err, (0, _ramda.clone)(fsmState));
        throw err;
    }

    return newFsmState;
  }

  function _computeOutputSinks(sinks$, /* OUT */accOutputSinks, sinkName) {
    accOutputSinks[sinkName] = sinks$.map(function (fsmState) {
      return (0, _ramda.defaultTo)($.empty(), fsmState.sinks[sinkName]);
    }).switch().tap(function (x) {
      return console.warn("FSM's " + sinkName + " -> ->", x);
    });

    return accOutputSinks;
  }

  var evaluateEvent = (0, _ramda.curry)(_evaluateEvent);
  var computeOutputSinks = (0, _ramda.curry)(_computeOutputSinks);

  return function fsmComponent(sources, settings) {
    // 1. Create array of events dealt with by the FSM
    // This will include :
    // - initial event
    // - events from `events` parameter
    // - action responses as found in `transitions`
    /** @type {Array.<Observable.<Object.<EventName, EventData>>>} */
    var eventsArray = (0, _ramda.values)((0, _ramda.mapObjIndexed)(computeAndLabelEvents(sources, settings), events));

    /** @type {String|ZeroDriver[][]} */
    var driverNameArrays = (0, _ramda.values)((0, _ramda.mapObjIndexed)(getDriverNames, transitions));

    /** @type {String[]} */
    var driverNameArray = removeZeroDriver((0, _ramda.uniq)((0, _ramda.flatten)(driverNameArrays)));
    /** @type {Array.<Observable.<Object.<SinkName, ActionResponse>>>} */
    var actionResponseObsArray = (0, _ramda.map)(function getActionResponseObs(driverName) {
      var prefixedActionResponse$ = sources[driverName].map(prefixWith(driverName));
      return debug ? prefixedActionResponse$.tap(function (x) {
        return console.warn('response event', x);
      }) : prefixedActionResponse$;
    }, driverNameArray);

    /** @type {Object.<EventName, EventData>} */
    var initialEvent = (0, _ramda.pipe)(prefixWith(_properties.INIT_EVENT_NAME), prefixWith(_properties.EVENT_PREFIX))(init_event_data);

    var fsmEvents = $.merge($.merge(eventsArray).map(prefixWith(_properties.EVENT_PREFIX)), $.merge(actionResponseObsArray).map(prefixWith(_properties.DRIVER_PREFIX))).startWith(initialEvent);

    // 2. Update the state of the state machine in function of the event
    // State machine state is represented by the following properties :
    // - internal_state : AWAITING_EVENTS | AWAITING_ACTION_RESPONSE
    // - external_state : State
    // - model : *
    // - current_event_data : EventData
    // - current_action_request_driver : DriverName

    /** @type {FSM_State}*/
    var clonedInitialModel = (0, _ramda.clone)(initial_model);
    var initialFSM_State = {
      internal_state: _properties.AWAITING_EVENTS,
      external_state: _properties.INIT_STATE,
      model: clonedInitialModel,
      clonedModel: clonedInitialModel,
      current_event_name: null,
      current_event_data: null,
      current_event_guard_index: null,
      current_action_request_driver: null,
      current_action_request: null,
      sinks: null
    };

    /** @type {Observable.<FSM_State>}*/
    var eventEvaluation$ = fsmEvents.scan(evaluateEvent(events, transitions, entryComponents, sources, settings), initialFSM_State);

    // 3. Pass output sinks onto the driver
    // Important! `shareReplay` is necessary here because of varying subscription timing
    // occurring inside a `switch` (i.e. subscriptions are delayed)
    /** @type {Observable.<Object.<SinkName, Observable.<*>>>}*/
    var sinks$ = eventEvaluation$.filter(function (fsmState) {
      return fsmState.sinks;
    }).tap(function (x) {
      return fsmSettings.debug && console.debug('new sinks to merge | ', x.sinks);
    }).shareReplay(1);

    /** @type {Object.<SinkName, Observable.<*>>}*/
    var outputSinks = (0, _ramda.reduce)(computeOutputSinks(sinks$), {}, sinkNames);

    return outputSinks;
  };
}

function computeSinkFromActionRequest(actionRequest, model, eventData) {
  var request = actionRequest.request(model, eventData);
  var driver = actionRequest.driver;

  return {
    request: request,
    driver: driver,
    sink: _defineProperty({}, driver, $.just(request))
  };
}

var wrapIfDebug = (0, _ramda.cond)([[(0, _ramda.prop)('debug'), (0, _ramda.evolve)({
  events: _utils2.decorateEventsWithLog,
  transitions: _utils2.decorateTransitionsWithLog,
  entryComponents: _utils2.decorateStateEntryWithLog
})], [_ramda.T, _ramda.identity]]);

// TODO : implmement history mechanism in this library
// TODO: actionable implementation of statecharts in hs https://github.com/steelbreeze/state.js
// TODO : fsm http://www.intersel.fr/assets_intersel_a/gitdemos/iFSM/Examples/Example_Slideshow.html
// TODO : use rxdb to replace firebase for the demo - it is totally offline, or
// pipeDb! https://github.com/pipelinedb/pipelinedb
// TODO : implement remaining contracts cf. doc
// TODO : move contrac for action guards and event guards (return boolean value) into debug:true
// so that when debug is false we have better performance
// TODO : also in the AOP, add the wrappign of guards if they send exceptions...
// TODO : same would be nice while saving to remote to show some message `pending...` = feature
// i.e. some model update for when FSM in between states
// The way to do this is to have ONE global pending internal FSM state which is associated to
// some action (think about how, if we update the view with a spinner, how?)
// TODO : 0. documenting features,
// TODO : 0. docuemnting with examples and graphs
// TODO : 1. convert a fsm data structure to a graphml or dot or tgf format
// TODO : define my own errors with nice contextual messages and parameters
// 0. TODO : Merge settings somehow (precedence and merge to define) with fsmSettings
// 0. TODO : check remaining contracts
// TODO : automatic actions through extra subject merged with the rest of event
//        automatic action are specified with the entry actions,
// attention to add the event at the end of the current tick (use cycle ordering of drivers?)
// TODO Doc : for init transition, set re_entry to true if one wants to have initial component
// display : this is due to INIT state being both target and current state at start-up
// doc that re-entry is useless if there is no transition defined with target=current state
// we could develop a contract that re-entry MUST not be there if no reentry, and MUST be
// defined if there is
// TODO : some action requests have no responses! feature: configuration property, can
// also be useful to implement pre-emptive event : no, pre-emptive are events of the same state,
// not the next event, hence to implement non RTC semantics
// TODO : 6. FSM re_entry should be lower, at action guard level when I put the target state
// DOC : the action responses will be obtained on the sources of the same name than the action sink
// NTH : change cycle dom driver to inject document, which would allow testing by mocking
// NTH : change cycle dom driver to use a real DOM vs. virtual DOM diff
// -> add event in team detail to update the model and re-entry
// ---- cf. https://github.com/patrick-steele-idem/morphdom,
// ----- https://github.com/patrick-steele-idem/morphdom/blob/master/docs/virtual-dom.md with
// -----   https://github.com/marko-js/marko-vdom for the matching vDom implementation
// TODO : 7. write a function which take the FSM config and get a graph out of it (mermaid? yed?)
// - mermaid uses dagre layout, : http://knsv.github.io/mermaid/flowchart.html
// - - allows to have more than flowchart, more kind of diagrams
// - - use specific text format (dagre-d3)
// - - css seems to be easy to modify, but little interactivity (tooltip etc.)
// - dagre-d3, interactivity to do by hand, https://github.com/cpettitt/dagre-d3/wiki
// - - use graphLib for inputting graph data :
// https://github.com/cpettitt/graphlib/wiki/API-Reference viz.js :
// https://github.com/mdaines/viz.js/ - - use graphviz/DOT language - - - howto :
// http://graphs.grevian.org/example - - - formal def :
// http://www.graphviz.org/content/dot-language vis.js : https://github.com/almende/vis, cf.
// network - - examples to look directly in the examples folder - - rich API, but hard to fathom -
// - uses simple format, but does not seem to be easy to have subgraphs with this format - - uses
// GEPHI https://marketplace.gephi.org/plugin/json-exporter/ - - looks quite complex but also lots
// of doc, not sure how to make it interactive RECOMMENDATION : 1. use viz/graphviz : graphviz is
// pretty neat, pretty standard format, can map to others 2. use dagre-d3 : it is a library, can
// customize anything with D3 BUT graphlib not exportable Conversion
// http://openconnecto.me/graph-services/convert/ - for isntance graphML (yed) to DOT (viz.js) NOTE
// - graphML is used by yed but also http://igraph.org/redirect.html
// TODO: can be used to have state chart logic for free : https://github.com/FrozenCanuck/Ki/blob/master/frameworks/foundation/system/statechart.js
// but I will have to add the events and change the formats (add transitions in objects instead
// of imperative this.go(state) ) and so on
// And also I should write tests, there are none...
// for google cloud spark back-end C:\Users\toshiba\AppData\Local\Google\Cloud SDK


// Dom driver
// modified       .tap(console.log.bind(console, `For namespace ${namespace}, emitting ${type} event:`))
// in C:\Users\toshiba\WebstormProjects\sparks-cyclejs\node_modules\cycle-snabbdom\lib\events.js
// Auth driver
// C:\Users\toshiba\WebstormProjects\sparks-cyclejs\node_modules\@sparksnetwork\cyclic-fire\dist\index.js
/*
 return rootElement$.first().flatMapLatest(function (rootElement) {
 if (!namespace || namespace.length === 0) {
 return (0, _fromEvent.fromEvent)(rootElement, type, useCapture);
 }
 var simulateBubbling = makeSimulateBubbling(namespace, rootElement);
 return (0, _fromEvent.fromEvent)(rootElement, type, useCapture).filter(simulateBubbling);
 })
 .tap(tapDomEvent(namespace, type))
 .share();
 };
 }

 function tapDomEvent(namespace, type) {
 return function tapDomEvent(ev) {
 const selection = pick(['clientX', 'clientY', 'path', 'ctrlKey', 'shiftKey', 'type'], ev)
 selection.targetClassName = ev.target ? ev.target.className : ""
 selection.targetId = ev.target ? ev.target.id : ""

 console.log(`For namespace ${namespace}, emitting ${type} event:`, selection)
 }
 }

 function pick(arrProps, obj) {
 let res = {}
 arrProps.forEach(key => res[key] = obj[key])

 return res
 }
*/
// Queue driver
// C:\Users\toshiba\WebstormProjects\sparks-cyclejs\node_modules\@sparksnetwork\cyclic-fire\dist\index.js
/*
return function (listenerKey) {
  return ChildAddedStream(srcRef.child(listenerKey))
    .tap(console.log.bind(console, `Response received for listenerKey (uid?) : ${listenerKey}`))
    .doAction(function (_ref2) {
      var key = _ref2.key;
      return deleteResponse(srcRef, listenerKey, key);
    });
};*/
// building query from fb api is simply mapping the args to chained fn calls
// C:\Users\toshiba\WebstormProjects\sparks-cyclejs\node_modules\@sparksnetwork\cyclic-fire\dist\index.js
// firebase driver
/*
var build = function build(args) {
  var stream = ValueStream(args.reduce(chain, ref))
    .tap(console.log.bind.console(`firebase query (args, ?)`, args))
    .replay(null, 1);
  var disposable = stream.connect();
  compositeDisposable.add(disposable);
  return stream;
};
*/
/*
action  :  "create"
domain  :  "Assignments"
uid    :    "nKyJt64tayMHJ0ucG0Q4A3gQogQ2"
payload  : {
  values    : {
    engagementKey    :    "-KaFkxJRCi60lUlzSsGx"
    oppKey    :    "-KX9OEk_l6mYeNFtS_mq"
    profileKey    :    "-KaFkwVPmISpvbyqC_1v"
    shiftKey    :    "-Ka35opxkYP01tVuiIgL"
    teamKey    :    "-KXA7guQXeOC7f8Zn8y1"
  }
}
*/
/*
Assignments : {
  -KFsXPXhpr7Jk4m0U_Dt :{
  engagementKey:"-KEyXx_POjDOx4Upiie1" // Engagments
  oppKey:"-KEh5MsHxpWFMa7n-38t" // Opps
  profileKey:    "-KEyXp9tXHdH7lBEBvGN" // Profiles
  shiftKey:    "-KFe9WZmSNbrt53fkZNr" // Shifts
  teamKey:    "-KEh7UYHKVz05E1HOsNY" // teams
  }
}
*/
/*
Profiles : {
-KR5Wv46CZeQ1iarHQHz : {
  dob
  email
  fullName
  isAdmin
  isEAP
  location
  phone
  portraitUrl
  uid
  }
}
*/
/*
Users : {
  JGhKXoFvpURntzyNBYzr5CAqeSp2:    "-KYLI7kW6nVlVRmFG5v2"
}
*/
/*
Shifts : {
  -KMwPqNFWph2BEBEvyIQ : {
    assigned : 2 // number of people assigned to that shift??
    date : "2016-07-30T00:00:00-04:00"
    end : "2016-07-30T16:00:00-04:00"
    hours : 4
    ownerProfileKey : "-KEMTVUivQTtyS8eYqKg" // ??
    people
    reserved
    start : "2016-07-30T12:00:00-04:00"
    teamKey : "-KMW9UO1IkMjRjc6ZqB2" // shift belongs to that team, Teams
  }
}
*/
var shifts$ = sources.teams$.map(map((0, _ramda.prop)('$key'))).flatMapLatest(compose(combineLatest, map(Shifts.query.byTeam(sources)))).map(_ramda.flatten).map(uniqBy((0, _ramda.prop)('$key'))).map(sortBy(compose(localTime, (0, _ramda.prop)('start'))));