'use strict';

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

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

var _ramda = require('ramda');

var _cycleSnabbdom = require('cycle-snabbdom');

var _rx = require('rx');

var Rx = _interopRequireWildcard(_rx);

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; } }

// Component typings
/**
 * @typedef {Observable} Stream
 */
/**
 * @typedef {Object.<string, Stream>} Sources
 */
/**
 * @typedef {Object.<string, Stream>} Sinks
 * NOTE : this type def is not perfect as we allow sometimes null values
 */
/**
 * @typedef {?Object.<string, ?Object>} Settings
 */
/**
 * // TODO : review types to match m current implementation
 * // I have example with only makeLocalSources set, so make all of them optional for now
 * @typedef {?Object} DetailedComponentDef
 * @property {?function(Sources, Settings)} makeLocalSources
 * @property {?function(Settings)} makeLocalSettings
 * @property {?function(Sources, Settings)} makeOwnSinks
 * @property {function(Sinks, Array<Sinks>, Settings)} mergeSinks
 * @property {?function(Sinks):Boolean} sinksContract
 * @property {?function(Sources):Boolean} sourcesContract
 */
/**
 * @typedef {?Object} ShortComponentDef
 * @property {?function(Sources, Settings)} makeLocalSources
 * @property {?function(Settings)} makeLocalSettings
 * @property {?function(Sources, Settings, Array<Component>)} makeAllSinks
 * @property {?function(Function, Array<Component>, Sources, Settings)} computeSinks
 * @property {?function(Sinks):Boolean} sinksContract
 * @property {?function(Sources):Boolean} sourcesContract
 */
/**
 * @typedef {function(Sources, Settings):Sinks} Component
 */

Rx.config.longStackSupport = true;
var $ = Rx.Observable;

var deepMerge = function deepMerge(a, b) {
  return (0, _ramda.is)(Object, a) && (0, _ramda.is)(Object, b) ? (0, _ramda.mergeWith)(deepMerge, a, b) : b;
};

// Configuration
var defaultMergeSinkConfig = {
  DOM: computeDOMSinkDefault,
  _default: computeSinkDefault
};

//////
// Helpers
/**
 * Merges the DOM nodes produced by a parent component with the DOM nodes
 * produced by children components, such that the parent DOM nodes
 * wrap around the children DOM nodes
 * For instance:
 * - parent -> div(..., [h2(...)])
 * - children -> [div(...), button(...)]
 * - result : div(..., [h2(...), div(...), button(...)])
 * @param {Sink} parentDOMSinkOrNull
 * @param {Array<Sink>} childrenSink
 * @param {Settings} settings
 * @returns {Observable<VNode>|Null}
 */
function computeDOMSinkDefault(parentDOMSinkOrNull, childrenSink, settings) {
  // We want `combineLatest` to still emit the parent DOM sink, even when
  // one of its children sinks is empty, so we modify the children sinks
  // to emits ONE `Null` value if it is empty
  // Note : in default function, settings parameter is not used
  var childrenDOMSinkOrNull = (0, _ramda.map)(_utils.emitNullIfEmpty, childrenSink);

  var allSinks = (0, _ramda.flatten)([parentDOMSinkOrNull, childrenDOMSinkOrNull]);
  var allDOMSinks = (0, _utils.removeNullsFromArray)(allSinks);

  // Edge case : none of the sinks have a DOM sink
  // That should not be possible as we come here only
  // when we detect a DOM sink
  if (allDOMSinks.length === 0) {
    throw 'mergeDOMSinkDefault: internal error!';
  }

  return $.combineLatest(allDOMSinks).tap(console.log.bind(console, 'mergeDOMSinkDefault: allDOMSinks')).map(mergeChildrenIntoParentDOM(parentDOMSinkOrNull));
}

function computeSinkDefault(parentDOMSinkOrNull, childrenSink, settings) {
  var allSinks = (0, _ramda.concat)([parentDOMSinkOrNull], childrenSink);

  // Nulls have to be removed as a given sink name will not be in all children
  // sinks. It is however guaranteed by the caller that the given sink
  // name will be in at least one of the children. Hence the merged array
  // is never empty
  return $.merge((0, _utils.removeNullsFromArray)(allSinks));
}

function mergeChildrenIntoParentDOM(parentDOMSink) {
  return function mergeChildrenIntoParentDOM(arrayVNode) {
    // We remove null elements from the array of vNode
    // We can have a null vNode emitted by a sink if that sink is empty
    var _arrayVNode = (0, _utils.removeEmptyVNodes)((0, _utils.removeNullsFromArray)(arrayVNode));
    (0, _utils.assertContract)((0, _utils.isArrayOf)(_utils.isVNode), [_arrayVNode], 'DOM sources must' + ' stream VNode objects! Got ' + _arrayVNode);

    if (parentDOMSink) {
      // Case : the parent sinks have a DOM sink
      // We want to put the children's DOM **inside** the parent's DOM
      // Two cases here :
      // - The parent's vNode has a `text` property :
      //   we move that text to a text vNode at first position in the children
      //   then we add the children's DOM in last position of the
      // existing parent's children
      // - The parent's vNode does not have a `text` property :
      //   we just add the children's DOM in last position of the exisitng
      //   parent's children
      // Note that this is specific to the snabbdom vNode data structure
      // Note that we defensively clone vNodes so the original vNode remains
      // immuted
      var parentVNode = (0, _ramda.clone)(_arrayVNode.shift());
      var childrenVNode = _arrayVNode;
      parentVNode.children = (0, _ramda.clone)(parentVNode.children) || [];

      // childrenVNode could be null if all children sinks are empty
      // observables, in which case we just return the parentVNode
      if (childrenVNode) {
        if (parentVNode.text) {
          parentVNode.children.splice(0, 0, {
            children: [],
            "data": {},
            "elm": undefined,
            "key": undefined,
            "sel": undefined,
            "text": parentVNode.text
          });
          parentVNode.text = undefined;
        }
        Array.prototype.push.apply(parentVNode.children, childrenVNode);
      }

      return parentVNode;
    } else {
      // Case : the parent sinks does not have a DOM sink
      // To avoid putting an extra `div` when there is only one vNode
      // we put the extra `div` only when there are several vNodes
      switch (_arrayVNode.length) {
        case 0:
          return null;
        case 1:
          return _arrayVNode[0];
        default:
          return (0, _cycleSnabbdom.div)(_arrayVNode);
      }
    }
  };
}

///////
// Helpers
function computeReducedSink(ownSinks, childrenSinks, localSettings, mergeSinks) {
  return function computeReducedSink(accReducedSinks, sinkName) {
    var mergeSinkFn = mergeSinks[sinkName] || defaultMergeSinkConfig[sinkName] || defaultMergeSinkConfig['_default'];
    (0, _utils.assertContract)(_utils.isMergeSinkFn, [mergeSinkFn], 'm : mergeSinkFn' + ' for sink ${sinkName} must be a function : check' + ' parameter or default merge function!');

    if (mergeSinkFn) {
      accReducedSinks[sinkName] = mergeSinkFn(ownSinks ? ownSinks[sinkName] : null, (0, _utils.projectSinksOn)(sinkName, childrenSinks), localSettings);
    }

    return accReducedSinks;
  };
}

// m :: Opt Component_Def -> Opt Settings -> [Component] -> Component
function m(componentDef, _settings, children) {
  /**
   * Returns a component specified by :
   * - a component definition object (nullable)
   * - outer settings (nullable)
   * - children components (or none)
   * Component definition properties :
   * - mergeSinks : computes resulting sinks or a specific sinks according to
   * configuration. See type information
   * - computeSinks : computes resulting sinks by executing the
   * children component and parent and merging the result
   * - sourcesContract : default to ...
   * - sinksContract : default to checking all sinks are observables or `null`
   * - settingsContract : default to do noting
   * - makeLocalSources : default -> null
   * - makeLocalSettings : default -> null
   * - makeOwnSinks : -> default null
   *
   * # Algorithm
   * The factored algorithm which derives sinks from sources is as follows :
   * - merging current `sources` with extra sources if any (configured with `makeLocalSources`)
   * - computing children sinks by executing the children components on the
   * merged sources
   * - computing some parent-component-specific sinks (i.e. that computation depends only on the
   * `sources` and `settings` parameter)
   * - merging the parent sinks with the children sinks
   *
   * There are two specification formats for the final component, according to the level of
   * granularity desired - the short spec and the detailed spec :
   * - short spec :
   *   one function `computeSinks` which computes the sinks from a children-independent sink
   *   factory, the sources, settings and children components. This is basically a big function
   *   which takes all information at disposal to compute the computed component sinks in one
   *   go. As such, this function gathers several concerns (source fetching, sinks creation,
   *   sinks merging, etc.).
   *   The rationale behind that function is that sometimes one needs complete control over the
   *   creation of the generation of the sinks for the computed component. Because the array of
   *   children components is passed directly (vs. passing the post-execution children
   *   components sinks), that function has total freedom over deciding how to compute
   *   and incorporate the children sinks.
   *   For the short spec, the relevant properties are :
   *   - `computeSinks`, `makeOwnSinks`,
   *   - the user-defined factories, the user-defined contracts
   * - detailed spec :
   *   For the detailed spec, the relevant properties are :
   *   - `makeOwnSinks`, `mergeSinks`,
   *   - the user-defined factories, the user-defined contracts
   *   TODO : in fact, the dichotomy short/detailed is not really valid - find another terminology
   *
   * Children components and local sinks factory functions will be called with the extended sources
   * (i.e. provided sources merged with locally computed sources, and settings resulting from
   * the three-way settings merge described thereafter).
   *
   * The goal of these functions is as follows:
   * - `makeOwnSinks(sources, settings)` : computation of children-independent sinks, i.e.
   * specific to that `m` factory call.
   * - `mergeSinks(ownSinks, childrenSinks, localSettings)` : outputs computed component sinks
   * as a result of the children-independent sinks, the children-generated sinks, and the
   * three-way merged settings received by the `m` factory and its run-time environment.
   *
   * To allow for an easier interface, and avoid repetition, in the absen TODO : detail the
   * merge DOM sinks svs. other sinks
   *
   *   several properties as detailed above
   * @param {DetailedComponentDef|ShortComponentDef} componentDef
   * @param {?Object} _settings
   * @param {Array<Component>} children
   *
   * @returns {Component}
   * @throws when type- and user-specified contracts are not satisfied
   *
   * # User-defined contracts
   * Contract functions allows to perform additional user-defined contract checking before computing
   * the component, for instance :
   * - check that sources have the expected properties
   * - check that the computed sinks have the expected type or properties
   *
   * # Sources
   * The factory-computer component is called with a `sources` argument which is the result of
   * the merge between the `sources` argument passed to the factory and the `sources` resulting
   * from the local sources factory (via `makeLocalSources`). In case of conflict, the local
   * sources factory has the lowest precedence vs. the factory sources.
   * It is important to note that this local sources factory function is called with a settings
   * parameter which ignores the local settings factory! Hence local settings cannot influence
   * the creation of sources.
   * In fact, there is almost no cases where an extra source might depend on settings. In the
   * vast majority of the cases, we expect the local sources factory to be independent of any
   * settings.
   *
   * # Settings
   * The output component returned by the `m` utility receives settings (at call time), termed in
   * what follows as inner settings. The `m` utility also receives settings, termed here as
   * outer settings. Any computation performed by the output component will be equivalent to
   * that of the same output component who would have received a merge of the inner and outer
   * settings. This allows the component factory `m` to parameterize/customize the behaviour of
   * its computed component. This in particular means that the outer settings take precedence
   * over the inner settings in case of conflict.
   *
   * Such merging conflicts are to be avoided in general. Having the computed component
   * behaviour depending on a parameter external to its definition means that one can no longer
   * reason about the component behaviour in isolation, but needs to know about the component's
   * context (position in the component tree).
   * There are however some valid cases when the equivalent of environment variables needs to be
   * passed down to components. Rather than explicitly passing those parameters to every
   * component individually down the component tree, it is enough to pass it once at some level,
   * and those parameters will be :
   * - visible at every lower level
   * - cannot be altered by lower-level components
   *
   * Those 'environment variables' should reflect concerns which are fairly orthogonal to the
   * component (leaf indexing, sinks signature, etc.), so that they do not interact with the
   * intended behaviour of the component.
   *
   * To complicate the matter further, as a part of the component definition, one can include
   * what is term here as computed settings (derived from the merge of inner and outer
   * settings). Those computed (at call time) settings are merged to the other two and have the
   * lowest precedence level of all. They aim at covering fairly narrow cases, and allow for
   * temporary customization of component behaviour (another call can result in a different
   * behaviour for the component).
   *
   * So :
   * - settings passed to the `m` factory are permanent and inherited by both the computed
   * component, and the children components which are part of the `m` factory definition
   * - the computed component is called with settings which are automatically passed down the
   * children components passed to the factory
   * - the children component behaviour can, if there is no conflict with existing settings,
   * be customized further by the local settings factory, which is a part of the `m` factory
   * definition
   *
   * TODO : this only clarifies the precedence between the factory and its computed component.
   * It might just be the opporiste of what is described there... because of tree evaluation order
   * There is a third case which is that the computed component receives also settings from
   * its upper hierarchy... To be detailed with examples, that\s the best given the three-way dance.
   * Also note that the settings passed down to children component from
   *
   * IMPLEMENTATION NOTES:
   * Source contracts are checked before merging incoming sources and user-configured sources
   * Settings contracts are checked on the final settings for the component, which is the result
   * of the merge of the outer settings passed through the `m` utility, and the inner settings
   * passed to the output component.
   *
   */
  console.groupCollapsed('Utils > m');
  console.log('componentDef, _settings, children', componentDef, _settings, children);
  // check signature
  /*
    const mSignature = [
      {componentDef: isNullableComponentDef},
      {settings: isNullableObject},
      {children: isArrayOf(isComponent)},
    ]
    assertSignature('m', arguments, mSignature)
  */

  (0, _utils.assertContract)(_utils.isNullableComponentDef, [componentDef], 'm : component definition is invalid!');
  (0, _utils.assertContract)(_utils.isNullableObject, [_settings], 'm : settings parameter is invalid!');
  (0, _utils.assertContract)((0, _utils.isArrayOf)(_utils.isComponent), [children], 'm : children components parameter is either not an array or some of the components passed do not have the expected format! Children components must at a minimum be functions');

  var makeLocalSources = componentDef.makeLocalSources,
      makeLocalSettings = componentDef.makeLocalSettings,
      makeOwnSinks = componentDef.makeOwnSinks,
      mergeSinks = componentDef.mergeSinks,
      computeSinks = componentDef.computeSinks,
      sinksContract = componentDef.sinksContract,
      sourcesContract = componentDef.sourcesContract,
      settingsContract = componentDef.settingsContract;

  // Set default values

  _settings = _settings || {};
  makeLocalSources = (0, _ramda.defaultTo)((0, _ramda.always)(null), makeLocalSources);
  makeLocalSettings = (0, _ramda.defaultTo)((0, _ramda.always)({}), makeLocalSettings);
  makeOwnSinks = (0, _ramda.defaultTo)((0, _ramda.always)(null), makeOwnSinks);
  mergeSinks = (0, _ramda.defaultTo)({}, mergeSinks);
  sinksContract = (0, _ramda.defaultTo)((0, _ramda.always)(true), sinksContract);
  sourcesContract = (0, _ramda.defaultTo)((0, _ramda.always)(true), sourcesContract);
  settingsContract = (0, _ramda.defaultTo)((0, _ramda.always)(true), settingsContract);

  console.groupEnd();

  return function m(sources, innerSettings) {
    console.groupCollapsed('m\'ed component > Entry');
    console.log('sources, innerSettings', sources, innerSettings);

    innerSettings = innerSettings || {};
    var mergedSettings = deepMerge(innerSettings, _settings);
    console.log('mergedSettings', mergedSettings);

    (0, _utils.assertSettingsContracts)(mergedSettings, settingsContract);

    (0, _utils.assertSourcesContracts)(sources, sourcesContract);

    // Computes and MERGES the extra sources which will be passed
    // to the children and this component
    // Extra sources are derived from the `sources`
    // received as input, which remain untouched
    var extendedSources = (0, _ramda.merge)(sources, makeLocalSources(sources, mergedSettings));

    // Note that per `merge` ramda spec. the second object's values
    // replace those from the first in case of key conflict
    var localSettings = deepMerge(makeLocalSettings(mergedSettings), mergedSettings);
    console.log('final settings', localSettings);

    var reducedSinks = void 0;

    // Case : computeSinks is defined
    if (computeSinks) {
      reducedSinks = computeSinks(makeOwnSinks, children, extendedSources, localSettings);
    }
    // Case : computeSinks is not defined, merge is defined in mergeSinks
    else {
        console.groupCollapsed('m\'ed component > makeOwnSinks');
        console.log('extendedSources, localSettings', extendedSources, localSettings);
        var ownSinks = makeOwnSinks(extendedSources, localSettings);
        console.groupEnd();

        console.group('m\'ed component > computing children sinks');
        var childrenSinks = (0, _ramda.map)(function (childComponent) {
          return childComponent(extendedSources, localSettings);
        }, children);
        console.groupEnd();

        (0, _utils.assertContract)(_utils.isOptSinks, [ownSinks], 'ownSinks must be a hash of observable sink');
        (0, _utils.assertContract)(_utils.isArrayOptSinks, [childrenSinks], 'childrenSinks must' + ' be an array of sinks');

        // Merge the sinks from children and one-s own...
        // Case : mergeSinks is defined through a function
        if ((0, _utils.isFunction)(mergeSinks)) {
          console.groupCollapsed('m\'ed component > (fn) mergeSinks');
          console.log('ownSinks, childrenSinks, localSettings', ownSinks, childrenSinks, localSettings);
          reducedSinks = mergeSinks(ownSinks, childrenSinks, localSettings);
          console.groupEnd();
        }
        // Case : mergeSinks is defined through an object
        else {
            var allSinks = (0, _ramda.flatten)((0, _utils.removeNullsFromArray)([ownSinks, childrenSinks]));
            var sinkNames = (0, _utils.getSinkNamesFromSinksArray)(allSinks);

            console.groupCollapsed('m\'ed component > (obj) mergeSinks');
            console.log('ownSinks, childrenSinks, localSettings,' + ' (fn) mergeSinks', ownSinks, childrenSinks, localSettings, mergeSinks);
            reducedSinks = (0, _ramda.reduce)(computeReducedSink(ownSinks, childrenSinks, localSettings, mergeSinks), {}, sinkNames);
            console.groupEnd();
          }
      }

    (0, _utils.assertSinksContracts)(reducedSinks, sinksContract);

    var tracedSinks = (0, _utils.trace)(reducedSinks, mergedSettings);
    // ... and add tracing information(sinkPath, timestamp, sinkValue/sinkError) after each sink
    // TODO : specify trace/debug/error generation information
    // This would ensure that errors are automatically and systematically
    //       caught in the component where they occur, and not
    //       interrupting the application implementation-wise, it might be
    //       necessary to add a `currentPath` parameter somewhere which
    //       carries the current path down the tree

    console.groupEnd();
    return tracedSinks;
  };
}

exports.m = m;