map.js

import {
  List
} from 'immutable';

import {
	isLeaf,
	asList
} from './utils';

/**
 * @module map
 */

const mapRecursive = (self, childPath, mapper, tree, options, nodePath = List()) => {
  const mapSelf = (self) => mapper(self, nodePath, tree);
  
  if(isLeaf(self, childPath)) {
    return options.onlyParents ? self : mapSelf(self);
  }
  
  const mappedChildren = self
    .getIn(childPath)
    .map((kid, key) => mapRecursive(kid, childPath, mapper, tree, options, nodePath.push(key)));
  
  const selfWithMappedChildren = childPath.isEmpty()
    ? self.merge(mappedChildren)
    : self.setIn(childPath, mappedChildren);
  
  return options.onlyLeaves
    ? selfWithMappedChildren
    : mapSelf(selfWithMappedChildren);
};

const mapRecursiveOutwards = (self, childPath, mapper, tree, options, nodePath = List()) => {
  if(options.onlyParents && isLeaf(self, childPath)) {
    return self;
  }

  var mappedSelf;
  if(options.onlyLeaves) {
    mappedSelf = self;
    if(isLeaf(mappedSelf, childPath)) {
      return mapper(self, nodePath, tree);
    }
  } else {
    mappedSelf = mapper(self, nodePath, tree);
    if(isLeaf(mappedSelf, childPath)) {
      return mappedSelf;
    }
  }

  return mappedSelf.updateIn(childPath, (children) => {
    return children.map((kid, key) => mapRecursiveOutwards(kid, childPath, mapper, tree, options, nodePath.push(key)));
  });
};


/**
 * Once fully applied, this iterates through all nodes in the provided tree, passing them all through a `mapper` function.
 * Nodes are processed branch by branch in the order that Immutable maps through the child iterables, inward from leaves to the root node.
 * Sibling nodes are processed in the order that Immutable iterates through collections.
 *
 * `TODO: note about care with child collections, note about using as reduce`
 * 
 * @param {Mapper} mapper The function to be called for every node in the tree, the results of which will be used to create the modified tree.
 * @param {ChildPath} [childPath=null] An `Array` or `List` of keys indicating where to find each node's children from within each node.
 * @return {InputFunction} A partially applied function which accepts a single tree `Iterable`, and returns the modified tree `Iterable`.
 */

function deepMap(mapper, childPath = null) {
    return (tree) => mapRecursive(tree, asList(childPath), mapper, tree, {});
}

/**
 * Once fully applied, this iterates through the leaf nodes in the provided tree, passing them all through a `mapper` function. Leaf nodes are nodes that have no child nodes.
 * Nodes are processed branch by branch, in the order that Immutable maps through the child iterables.
 *
 * @param {Mapper} mapper The function to be called for every leaf node in the tree, the results of which will be used to create the modified tree.
 * @param {ChildPath} [childPath=null] An `Array` or `List` of keys indicating where to find each node's children from within each node.
 * @return {InputFunction} A partially applied function which accepts a single tree `Iterable`, and returns the modified tree `Iterable`.
 */

function deepMapLeaves(mapper, childPath = null) {
    return (tree) => mapRecursive(tree, asList(childPath), mapper, tree, {onlyLeaves: true});
}

/**
 * Once fully applied, this iterates through the parent nodes in the provided tree, passing them all through a `mapper` function. Parent nodes are nodes that have child nodes.
 * Nodes are processed branch by branch in the order that Immutable maps through the child iterables, inward from leaves to the root node.
 *
 * @param {Mapper} mapper The function to be called for every parent node in the tree.
 * the results of which will be used to create the modified iterable.
 * @param {ChildPath} [childPath=null] An `Array` or `List` of keys indicating where to find each node's children from within each node.
 * @return {InputFunction} A partially applied function which accepts a single tree `Iterable`, and returns the modified tree `Iterable`.
 */

function deepMapParents(mapper, childPath = null) {
    return (tree) => mapRecursive(tree, asList(childPath), mapper, tree, {onlyParents: true});
}

/**
 * Once fully applied, this iterates through all nodes in the provided tree, passing them all through a `mapper` function.
 * Nodes are processed branch by branch in the order that Immutable maps through the child iterables, outwards from the root node to the leaf nodes.
 * Sibling nodes are processed in the order that Immutable iterates through collections.
 *
 * Because this moves from the root node outwards this function behaves quite differently to the other deep map functions.
 *
 * - If you modify a node's children, then those children will already be modified by the time they are called by the `mapper` function.
 * - Because of the above, if a node removes or renames its own children iterable then those children will not be passed through the `mapper` function.
 *
 * @param {Mapper} mapper The function to be called for every node in the tree, the results of which will be used to create the modified tree.
 * @param {ChildPath} [childPath=null] An `Array` or `List` of keys indicating where to find each node's children from within each node.
 * @return {InputFunction} A partially applied function which accepts a single tree `Iterable`, and returns the modified tree `Iterable`.
 */

function deepMapOutwards(mapper, childPath = null) {
    return (tree) => mapRecursiveOutwards(tree, asList(childPath), mapper, tree, {});
}

/**
 * Once fully applied, this iterates through the leaf nodes in the provided tree, passing them all through a `mapper` function.
 * Nodes are processed branch by branch in the order that Immutable maps through the child iterables, outwards from the root node to the leaf nodes.
 * Sibling nodes are processed in the order that Immutable iterates through collections.
 *
 * @param {Mapper} mapper The function to be called for every leaf node in the tree, the results of which will be used to create the modified tree.
 * @param {ChildPath} [childPath=null] An `Array` or `List` of keys indicating where to find each node's children from within each node.
 * @return {InputFunction} A partially applied function which accepts a single tree `Iterable`, and returns the modified tree `Iterable`.
 */

function deepMapLeavesOutwards(mapper, childPath = null) {
    return (tree) => mapRecursiveOutwards(tree, asList(childPath), mapper, tree, {onlyLeaves: true});
}


/**
 * Once fully applied, this iterates through the parent nodes in the provided tree, passing them all through a `mapper` function.
 * Nodes are processed branch by branch in the order that Immutable maps through the child iterables, outwards from the root node to the leaf nodes.
 * Sibling nodes are processed in the order that Immutable iterates through collections.
 *
 * Because this moves from the root node outwards this function behaves quite differently to the other deep map functions.
 *
 * - If you modify a node's children, then those children will already be modified by the time they are called by the `mapper` function.
 * - Because of the above, if a node removes or renames its own children iterable then those children will not be passed through the `mapper` function.
 *
 * @param {Mapper} mapper The function to be called for every parent node in the tree, the results of which will be used to create the modified tree.
 * @param {ChildPath} [childPath=null] An `Array` or `List` of keys indicating where to find each node's children from within each node.
 * @return {InputFunction} A partially applied function which accepts a single tree `Iterable`, and returns the modified tree `Iterable`.
 */

function deepMapParentsOutwards(mapper, childPath = null) {
    return (tree) => mapRecursiveOutwards(tree, asList(childPath), mapper, tree, {onlyParents: true});
}

export {
  deepMap,
  deepMapLeaves,
  deepMapParents,
  deepMapOutwards,
  deepMapLeavesOutwards,
  deepMapParentsOutwards
}