import * as _ from 'lodash';

import {
  buildFormGroupTemplate, checkInlineType, copy, forEach, getControl,
  getFromSchema, getInputType, hasOwn, inArray, isArray, isEmpty,
  isInputRequired, isNumber, isObject, isDefined, isString, JsonPointer,
  toTitleCase, updateInputOptions
} from './index';

/**
 * Layout function library:
 *
 * buildLayout:            Builds a complete layout from an input layout and schema
 *
 * buildLayoutFromSchema:  Builds a complete layout entirely from an input schema
 *
 * mapLayout:
 *
 * buildTitleMap:
 */

/**
 * 'buildLayout' function
 *
 * @param {any} jsf
 * @return {any[]}
 */
export function buildLayout(jsf: any, widgetLibrary: any): any[] {
  let hasSubmitButton = !JsonPointer.get(jsf, '/globalOptions/addSubmit');
  let formLayout = mapLayout(jsf.layout, (layoutItem, index, layoutPointer) => {
    let currentIndex: number = index;
    let newNode: any = {};
    if (isObject(layoutItem)) {
      newNode = layoutItem;
    } else if (JsonPointer.isJsonPointer(layoutItem)) {
      newNode.dataPointer = layoutItem;
    } else if (isString(layoutItem)) {
      newNode.key = layoutItem;
    } else {
      console.error('buildLayout error: Form layout element not recognized:');
      console.error(layoutItem);
      return null;
    }
    Object.assign(newNode, {
      _id: _.uniqueId(),
      layoutPointer: layoutPointer.replace(/\/\d+/g, '/-'),
      options: {},
    });
    let itemSchema: any = null;

    // If newNode does not have a dataPointer, try to find an equivalent
    if (!hasOwn(newNode, 'dataPointer')) {

      // If newNode has a key, change it to a dataPointer
      if (hasOwn(newNode, 'key')) {
        if (newNode.key === '*') {
          newNode.dataPointer = newNode.key;
        } else if (JsonPointer.isJsonPointer(newNode.key)) {
          newNode.dataPointer = JsonPointer.compile(newNode.key);
        } else {
          newNode.dataPointer =
            JsonPointer.compile(JsonPointer.parseObjectPath(newNode.key), '-');
        }
        delete newNode.key;

      // If newNode is an array, search for dataPointer in child nodes
      } else if (hasOwn(newNode, 'type') && newNode.type.slice(-5) === 'array') {
        const findDataPointer = (items) => {
          if (items === null || typeof items !== 'object') { return; }
          if (hasOwn(items, 'dataPointer')) { return items.dataPointer; }
          if (isArray(items.items)) {
            for (let item of items.items) {
              if (hasOwn(item, 'dataPointer') &&
                item.dataPointer.indexOf('/-') !== -1
              ) {
                return item.dataPointer;
              }
              if (hasOwn(item, 'items')) {
                const searchItem = findDataPointer(item);
                if (searchItem) { return searchItem; }
              }
            }
          }
        };
        const childDataPointer = findDataPointer(newNode);
        if (childDataPointer) {
          newNode.dataPointer =
            childDataPointer.slice(0, childDataPointer.lastIndexOf('/-'));
        }
      }
    }

    if (hasOwn(newNode, 'dataPointer')) {
      if (newNode.dataPointer === '*') {
        return buildLayoutFromSchema(
          jsf, widgetLibrary, newNode.layoutPointer.slice(0, -2)
        );
      }
      newNode.dataPointer =
        JsonPointer.toGenericPointer(newNode.dataPointer, jsf.arrayMap);
      const LastKey: string = JsonPointer.toKey(newNode.dataPointer);
      if (isString(LastKey) && LastKey !== '-') {
        newNode.name = LastKey;
      }
      if (!jsf.dataMap.has(newNode.dataPointer)) {
        jsf.dataMap.set(newNode.dataPointer, new Map);
      } else if (
        jsf.dataMap.get(newNode.dataPointer).has('schemaPointer')
      ) {
        itemSchema = JsonPointer.get(
          jsf.schema,
          jsf.dataMap.get(newNode.dataPointer).get('schemaPointer')
        );
      } else {
        itemSchema = getFromSchema(jsf.schema, newNode.dataPointer);
      }
      if (itemSchema) {
        if (!hasOwn(newNode, 'type')) {
          newNode.type = getInputType(itemSchema, newNode);
        } else if (!widgetLibrary.hasWidget(newNode.type)) {
          const oldWidgetType = newNode.type;
          newNode.type = getInputType(itemSchema, newNode);
          console.error('error: widget type "' + oldWidgetType +
            '" not found in library. Replacing with "' + newNode.type + '".');
        } else {
          newNode.type = checkInlineType(newNode.type, itemSchema, newNode);
        }
        newNode.dataType = itemSchema.type;
        updateInputOptions(newNode, itemSchema, jsf);

        // Present checkboxes as single control, rather than array
        if (newNode.type === 'checkboxes' && hasOwn(itemSchema, 'items')) {
          updateInputOptions(newNode, itemSchema.items, jsf);
        } else if (itemSchema.type === 'array' && hasOwn(itemSchema, 'items')) {
          if (isArray(itemSchema.items)) {
            newNode.tupleItems = itemSchema.items.length;
            if (hasOwn(itemSchema, 'additionalItems')) {
              newNode.listItems = hasOwn(itemSchema, 'maxItems') ?
                itemSchema.maxItems - itemSchema.items.length : true;
            } else {
              newNode.listItems = false;
            }
          } else {
            newNode.tupleItems = false;
            newNode.listItems = itemSchema.maxItems || true;
          }
        }
        if (!newNode.options.title && newNode.options.legend) {
          newNode.options.title = newNode.options.legend;
        } else if (!newNode.options.title && !/^\d+$/.test(newNode.name)) {
          newNode.options.title = toTitleCase(newNode.name.replace(/_/g, ' '));
        }
        if (isInputRequired(jsf.schema, newNode.dataPointer)) {
          newNode.options.required = true;
          jsf.fieldsRequired = true;
        }
      } else {
        // TODO: create item in FormGroup model from layout key (?)
        updateInputOptions(newNode, {}, jsf);
      }

      if (hasOwn(newNode.options, 'copyValueTo')) {
        if (typeof newNode.options.copyValueTo === 'string') {
          newNode.options.copyValueTo = [newNode.options.copyValueTo];
        }
        if (isArray(newNode.options.copyValueTo)) {
          newNode.options.copyValueTo = newNode.options.copyValueTo.map(item =>
            JsonPointer.isJsonPointer(item) ?
              JsonPointer.compile(item) :
              JsonPointer.compile(JsonPointer.parseObjectPath(item), '-')
          );
        }
      }

      newNode.widget = widgetLibrary.getWidget(newNode.type);
      jsf.dataMap.get(newNode.dataPointer).set('inputType', newNode.type);
      jsf.dataMap.get(newNode.dataPointer).set('widget', newNode.widget);

      if (newNode.dataType === 'array' && hasOwn(newNode, 'items')) {
        if (newNode.options.required && !newNode.minItems) { newNode.minItems = 1; }
        let arrayPointer: string = newNode.dataPointer + '/-';
        if (!jsf.dataMap.has(arrayPointer)) {
          jsf.dataMap.set(arrayPointer, new Map);
        }
        jsf.dataMap.get(arrayPointer).set('inputType', 'section');

        // Fix insufficiently nested array item groups
        if (newNode.items.length > 1) {
          let arrayItemGroup = [];
          let arrayItemGroupTemplate = [];
          let newIndex = 0;
          for (let i = newNode.items.length - 1, l = 0; i >= l; i--) {
            let subItem = newNode.items[i];
            if (hasOwn(subItem, 'dataPointer') &&
              subItem.dataPointer.slice(0, arrayPointer.length) === arrayPointer
            ) {
              let arrayItem = newNode.items.splice(i, 1)[0];
              let arrayItemTemplate = mapLayout([arrayItem], templateItem => {
                templateItem.layoutPointer = templateItem.layoutPointer
                  .replace(newNode.layoutPointer, newNode.layoutPointer + '/items/-');
                return templateItem;
              })[0];
              arrayItemGroupTemplate.unshift(arrayItemTemplate);
              arrayItem.dataPointer = newNode.dataPointer + '/-' +
                arrayItem.dataPointer.slice(arrayPointer.length);
              arrayItem.layoutPointer = newNode.layoutPointer + '/items/-/items/-';
              arrayItemGroup.unshift(arrayItem);
              newIndex++;
            } else {
              subItem.arrayItem = true;
              // TODO: Check schema to get arrayItemType and removable
              subItem.arrayItemType = 'list';
              subItem.removable = newNode.options.removable || !newNode.options.minItems;
            }
          }
          if (arrayItemGroup.length) {
            newNode.items.push({
              arrayItem: true,
              items: arrayItemGroup,
              layoutPointer: newNode.layoutPointer + '/items/-',
              options: {
                arrayItemType: newNode.tupleItems > newNode.items.length ?
                  'tuple' : 'list',
                removable: newNode.options.removable !== false &&
                  (newNode.options.minItems || 0) <= newNode.items.length,
              },
              dataPointer: newNode.dataPointer + '/-',
              type: 'fieldset',
              widget: widgetLibrary.getWidget('fieldset'),
            });
          }
        } else {
          newNode.items[0].arrayItem = true;
          if (!newNode.items[0].dataPointer) {
            newNode.items[0].dataPointer =
              JsonPointer.toGenericPointer(arrayPointer, jsf.arrayMap);
          }
          if (newNode.options.minItems) {
            newNode.items[0].options.removable = false;
          } else if (!JsonPointer.has(newNode, '/items/0/options/removable')) {
            newNode.items[0].options.removable = true;
          }
          newNode.items[0].options.arrayItemType =
            newNode.tupleItems ? 'tuple' : 'list';
        }

        // TODO: check maxItems to verify adding new items is OK, and check
        // additionalItems for whether there is a different schema for new items
        if (newNode.options.addable !== false) {
          jsf.layoutRefLibrary[arrayPointer] =
            _.cloneDeep(newNode.items[newNode.items.length - 1]);
          const initialNodeData =
            JsonPointer.get(jsf.initialValues, newNode.dataPointer);
          if (isArray(initialNodeData) &&
            initialNodeData.length > newNode.items.length
          ) {
            for (let i = newNode.items.length, l = initialNodeData.length; i < l; i++) {
              newNode.items
                .push(_.cloneDeep(jsf.layoutRefLibrary[arrayPointer]));
            }
          }
          let buttonText: string = 'Add';
          if (newNode.options.title) {
            buttonText += ' ' + newNode.options.title;
          } else if (newNode.name && !/^\d+$/.test(newNode.name)) {
            buttonText += ' ' + toTitleCase(newNode.name.replace(/_/g, ' '));

          // If newNode doesn't have a title, look for title of parent array item
          } else {
            const parentSchema =
              getFromSchema(jsf.schema, newNode.dataPointer, true);
            if (hasOwn(parentSchema, 'title')) {
              buttonText += ' to ' + parentSchema.title;
            }
          }
          const dataPointer = JsonPointer.toGenericPointer(arrayPointer, jsf.arrayMap);
          let newNodeRef: any = {
            arrayItem: true,
            dataPointer: dataPointer,
            layoutPointer: newNode.layoutPointer + '/items/-',
            listItems: newNode.listItems,
            options: {
              arrayItemType: 'list',
              removable: !!newNode.options.removable,
              title: buttonText,
            },
            tupleItems: newNode.tupleItems,
            type: '$ref',
            widget: widgetLibrary.getWidget('$ref'),
            $ref: dataPointer,
          };
          if (isDefined(newNode.options.maxItems)) {
            newNodeRef.options.maxItems = newNode.options.maxItems;
          }
          if (isString(JsonPointer.get(newNode, '/style/add'))) {
            newNodeRef.options.fieldStyle = newNode.style.add;
            delete newNode.style.add;
            if (isEmpty(newNode.style)) { delete newNode.style; }
          }
          newNode.items.push(newNodeRef);
        }
      } else {
        newNode.arrayItem = false;
      }
    } else if (hasOwn(newNode, 'type') || hasOwn(newNode, 'items')) {
      const parentType: string =
        JsonPointer.get(jsf.layout, layoutPointer, 0, -2).type;
      if (!hasOwn(newNode, 'type')) {
        newNode.type =
          inArray(parentType, ['tabs', 'tabarray']) ? 'tab' : 'fieldset';
      }
      newNode.arrayItem = parentType === 'array';
      newNode.widget = widgetLibrary.getWidget(newNode.type);
      updateInputOptions(newNode, {}, jsf);
    }
    if (newNode.type === 'submit') { hasSubmitButton = true; }
    return newNode;
  });
  if (!hasSubmitButton) {
    formLayout.push({
      options: {
        title: 'Submit',
      },
      type: 'submit',
      widget: widgetLibrary.getWidget('submit'),
    });
  }
  return formLayout;
}

/**
 * 'buildLayoutFromSchema' function
 *
 * @param {any} jsf -
 * @param {number = 0} layoutIndex -
 * @param {string = ''} layoutPointer -
 * @param {string = ''} schemaPointer -
 * @param {string = ''} dataPointer -
 * @param {boolean = false} arrayItem -
 * @param {string = null} arrayItemType -
 * @param {boolean = null} removable -
 * @param {boolean = false} forRefLibrary -
 * @return {any}
 */
export function buildLayoutFromSchema(
  jsf: any, widgetLibrary: any, layoutPointer: string = '',
  schemaPointer: string = '', dataPointer: string = '',
  arrayItem: boolean = false, arrayItemType: string = null,
  removable: boolean = null, forRefLibrary: boolean = false
): any {
  const schema = JsonPointer.get(jsf.schema, schemaPointer);
  if (!hasOwn(schema, 'type') && !hasOwn(schema, 'x-schema-form') &&
    !hasOwn(schema, '$ref')) { return null; }
  const newNodeType: string = getInputType(schema);
  let newNode: any = {
    _id: _.uniqueId(),
    arrayItem: arrayItem,
    dataPointer: JsonPointer.toGenericPointer(dataPointer, jsf.arrayMap),
    dataType: schema.type || (hasOwn(schema, '$ref') ? '$ref' : null),
    layoutPointer: layoutPointer.replace(/\/\d+/g, '/-') || '/-',
    options: {},
    type: newNodeType,
    widget: widgetLibrary.getWidget(newNodeType),
  };
  const lastDataKey = JsonPointer.toKey(newNode.dataPointer);
  if (lastDataKey !== '-') { newNode.name = lastDataKey; }
  if (newNode.arrayItem) {
    newNode.options.arrayItemType = arrayItemType;
    newNode.options.removable = removable;
  }
  if (dataPointer !== '') {
    if (!jsf.dataMap.has(newNode.dataPointer)) {
      jsf.dataMap.set(newNode.dataPointer, new Map);
    }
    jsf.dataMap.get(newNode.dataPointer).set('schemaPointer', schemaPointer);
    jsf.dataMap.get(newNode.dataPointer).set('inputType', newNode.type);
    jsf.dataMap.get(newNode.dataPointer).set('widget', newNode.widget);
  }
  updateInputOptions(newNode, schema, jsf);
  if (!newNode.options.title && newNode.options.legend) {
    newNode.options.title = newNode.options.legend;
  } else if (!newNode.options.title && newNode.name && !/^\d+$/.test(newNode.name)) {
    newNode.options.title = toTitleCase(newNode.name.replace(/_/g, ' '));
  }
  if (newNode.dataType === 'object') {
    let newFieldset: any[] = [];
    let newKeys: string[] = [];
    if (isObject(schema.properties)) {
      newKeys = isArray(schema.properties['ui:order']) ?
        schema['properties']['ui:order'] : Object.keys(schema['properties']);
    } else if (hasOwn(schema, 'additionalProperties')) {
      return null;
      // TODO: Figure out what to do with additionalProperties
      // ... possibly provide a way to enter both key names and values?
    }
    for (let key of newKeys) {
      if (hasOwn(schema.properties, key)) {
        let newLayoutPointer: string;
        if (newNode.layoutPointer === '' && !forRefLibrary) {
          newLayoutPointer = '/-';
        } else {
          newLayoutPointer = newNode.layoutPointer + '/items/-';
        }
        let innerItem = buildLayoutFromSchema(
          jsf, widgetLibrary,
          newLayoutPointer,
          schemaPointer + '/properties/' + key,
          dataPointer + '/' + key,
          false, null, null, forRefLibrary
        );
        if (innerItem) {
          if (isInputRequired(schema, '/' + key)) {
            innerItem.options.required = true;
            jsf.fieldsRequired = true;
          }
          newFieldset.push(innerItem);
        }
      }
    }
    // if (dataPointer === '' && !forRefLibrary) {
    //   newNode = newFieldset;
    // } else {
      newNode.items = newFieldset;
    // }
  } else if (newNode.dataType === 'array') {
    newNode.items = [];
    let templateArray: any[] = [];
    if (!forRefLibrary) {
      const templateControl: any =
        getControl(jsf.formGroupTemplate, dataPointer);
      if (hasOwn(templateControl, 'controls')) {
        templateArray = templateControl['controls'];
      }
    }
    if (!newNode.minItems && isInputRequired(jsf.schema, schemaPointer)) {
      newNode.minItems = 1;
    }
    const minItems: number = newNode.minItems || 0;
    const maxItems: number = newNode.maxItems || 1000000;
    if (isDefined(newNode.options.removable)) {
      removable = newNode.options.removable;
    } else if (!isDefined(removable)) {
      removable = true;
    }
    let additionalItems: any = null;
    if (isArray(schema.items)) { // 'items' is an array = tuple items
      newNode.tupleItems = schema.items.length;
      if (hasOwn(schema, 'additionalItems')) {
        newNode.listItems = hasOwn(schema, 'maxItems') ?
          schema.maxItems - schema.items.length : true;
      } else {
        newNode.listItems = false;
      }
      newNode.items = _.filter(_.map(schema.items, (item: any, i) => {
        return buildLayoutFromSchema(
          jsf, widgetLibrary,
          newNode.layoutPointer + '/items/-',
          schemaPointer + '/items/' + i,
          dataPointer + '/' + i,
          true, 'tuple', removable && i >= minItems, forRefLibrary
        );
      }));
      if (newNode.items.length < maxItems &&
        hasOwn(schema, 'additionalItems') && isObject(schema.additionalItems)
      ) { // 'additionalItems' is an object = additional list items (after tuple items)
        if (newNode.items.length < templateArray.length) {
          for (let i = newNode.items.length, l = templateArray.length; i < l; i++) {
            newNode.items.push(buildLayoutFromSchema(
              jsf, widgetLibrary,
              newNode.layoutPointer + '/items/-',
              schemaPointer + '/additionalItems',
              dataPointer + '/' + i,
              true, 'list', removable && i >= minItems, forRefLibrary
            ));
          }
        } else if (newNode.items.length > templateArray.length) {
          for (let i = templateArray.length, l = newNode.items.length; i < l; i++) {
            templateArray.push(buildFormGroupTemplate(
              jsf, null, false,
              schemaPointer + '/additionalItems',
              dataPointer + '/' + i,
              JsonPointer.toControlPointer(jsf.formGroupTemplate, dataPointer + '/' + i)
            ));
          }
        }
        if (newNode.items.length < maxItems && newNode.options.addable !== false &&
          JsonPointer.get(newNode.items[newNode.items.length - 1], '/type') !== '$ref'
        ) {
          additionalItems = buildLayoutFromSchema(
            jsf, widgetLibrary,
            newNode.layoutPointer + '/items/-',
            schemaPointer + '/additionalItems',
            dataPointer + '/-',
            true, 'list', removable, forRefLibrary
          );
        }
      }
    } else { // 'items' is an object = list items only (no tuple items)
      newNode.tupleItems = false;
      newNode.listItems = schema.maxItems || true;
      for (let i = 0, l = Math.max(templateArray.length, minItems, 1); i < l; i++) {
        newNode.items.push(buildLayoutFromSchema(
          jsf, widgetLibrary,
          newNode.layoutPointer + '/items/-',
          schemaPointer + '/items',
          dataPointer + '/' + i,
          true, 'list', removable && i >= minItems, forRefLibrary
        ));
      }
      if (newNode.items.length < maxItems && newNode.options.addable !== false &&
        JsonPointer.get(newNode.items[newNode.items.length - 1], '/type') !== '$ref'
      ) {
        additionalItems = buildLayoutFromSchema(
          jsf, widgetLibrary,
          newNode.layoutPointer + '/items/-',
          schemaPointer + '/items',
          dataPointer + '/-',
          true, 'list', removable, forRefLibrary
        );
      }
    }

    // If addable items, save to layoutRefLibrary, and add $ref item to layout
    if (additionalItems) {
      jsf.layoutRefLibrary[dataPointer + '/-'] = additionalItems;
      delete jsf.layoutRefLibrary[dataPointer + '/-']['key'];
      delete jsf.layoutRefLibrary[dataPointer + '/-']['name'];
      let buttonText: string = 'Add ';
      if (additionalItems.options.title) {
        buttonText += additionalItems.options.title;
      } else if (schema.title) {
        buttonText += 'to ' + schema.title;
      } else {
        buttonText += 'to ' +
          toTitleCase(JsonPointer.toKey(dataPointer).replace(/_/g, ' '));
      }
      let newNodeRef: any = {
        arrayItem: true,
        dataPointer: dataPointer + '/-',
        layoutPointer: newNode.layoutPointer + '/items/-',
        listItems: newNode.listItems,
        options: {
          arrayItemType: 'list',
          removable: false,
          title: buttonText,
        },
        tupleItems: newNode.tupleItems,
        type: '$ref',
        widget: widgetLibrary.getWidget('$ref'),
        $ref: dataPointer + '/-',
      };
      if (isDefined(newNode.options.maxItems)) {
        newNodeRef.options.maxItems = newNode.options.maxItems;
      }
      newNode.items.push(newNodeRef);
    } else if (
      JsonPointer.get(newNode.items[newNode.items.length - 1], '/type') === '$ref'
    ) {
      Object.assign(newNode.items[newNode.items.length - 1], {
        listItems: newNode.listItems,
        tupleItems: newNode.tupleItems,
      });
      if (
        isNumber(JsonPointer.get(jsf.schema, schemaPointer, 0, -1).maxItems)
      ) {
        newNode.items[newNode.items.length - 1].options.maxItems =
          JsonPointer.get(jsf.schema, schemaPointer, 0, -1).maxItems;
      }
    }
  } else if (newNode.dataType === '$ref') {
    const schemaRef: string = JsonPointer.compile(schema.$ref);
    let buttonText: string = 'Add';
    if (newNode.options.title) {
      buttonText += ' ' + newNode.options.title;
    } else if (newNode.name && !/^\d+$/.test(newNode.name)) {
      buttonText += ' ' + toTitleCase(newNode.name.replace(/_/g, ' '));

    // If newNode doesn't have a title, look for title of parent array item
    } else if (
      hasOwn(JsonPointer.get(jsf.schema, schemaPointer, 0, -1), 'title')
    ) {
      buttonText += ' to ' +
        JsonPointer.get(jsf.schema, schemaPointer, 0, -1).title;
    }
    Object.assign(newNode, {
      recursiveReference: true,
      widget: widgetLibrary.getWidget('$ref'),
      $ref: schemaRef,
    });
    Object.assign(newNode.options, {
      removable: false,
      title: buttonText,
    });
    if (isNumber(JsonPointer.get(jsf.schema, schemaPointer, 0, -1).maxItems)) {
      newNode.options.maxItems =
        JsonPointer.get(jsf.schema, schemaPointer, 0, -1).maxItems;
    }

    // Build dataRecursiveRefMap
    let genericDataPointer =
      JsonPointer.toGenericPointer(newNode.dataPointer, jsf.arrayMap);
    // TODO: Replace the following by checking to see if the parent element is
    // an array, and only removing the index if allowed
    genericDataPointer = genericDataPointer.replace(/\/\d+$/, '/-');
    if (!forRefLibrary) {

      // Is schema $ref a subset of dataPointer?
      // If yes, map dataPointer and schema $ref as a recursive reference
      if (JsonPointer.isSubPointer(schemaRef, genericDataPointer)) {
        jsf.dataRecursiveRefMap.set(genericDataPointer, schemaRef);

      // If no, add a partial reference now, so a full reference can be added later
      } else {
        jsf.dataRecursiveRefMap.set(schemaRef, genericDataPointer);
      }

    // If partial reference already exists,
    // use current and previous dataPointers to create a full reference
    } else if (jsf.dataRecursiveRefMap.has(schemaRef) &&
      !jsf.dataRecursiveRefMap.has(jsf.dataRecursiveRefMap.get(schemaRef))
    ) {
      if (genericDataPointer ===
        jsf.dataRecursiveRefMap.get(schemaRef).slice(-genericDataPointer.length)
      ) {
        jsf.dataRecursiveRefMap.set(
          jsf.dataRecursiveRefMap.get(schemaRef),
          jsf.dataRecursiveRefMap.get(schemaRef).slice(0, -genericDataPointer.length)
        );
      } else {
        jsf.dataRecursiveRefMap.set(
          jsf.dataRecursiveRefMap.get(schemaRef) + genericDataPointer,
          jsf.dataRecursiveRefMap.get(schemaRef)
        );
      }
    }

    // Add layout template to layoutRefLibrary
    if (!hasOwn(jsf.layoutRefLibrary, schemaRef)) {
      // Set to null first to prevent recursive reference from causing endless loop
      jsf.layoutRefLibrary[schemaRef] = null;
      const newLayout: any = buildLayoutFromSchema(
        jsf, widgetLibrary, '', schemaRef, '',
        newNode.arrayItem, newNode.arrayItemType, true, true
      );
      if (newLayout) {
        jsf.layoutRefLibrary[schemaRef] = newLayout;
      } else {
        delete jsf.layoutRefLibrary[schemaRef];
      }
    }
  }
  return newNode;
}

/**
 * 'mapLayout' function
 *
 * Creates a new layout by running each element in an existing layout through
 * an iteratee. Recursively maps within array elements 'items' and 'tabs'.
 * The iteratee is invoked with four arguments: (value, index, layout, path)
 *
 * THe returned layout may be longer (or shorter) then the source layout.
 *
 * If an item from the source layout returns multiple items (as '*' usually will),
 * this function will keep all returned items in-line with the surrounding items.
 *
 * If an item from the source layout causes an error and returns null, it is
 * simply skipped, and the function will still return all non-null items.
 *
 * @param {any[]} layout - the layout to map
 * @param {(v: any, i?: number, l?: any, p?: string) => any}
 *   function - the funciton to invoke on each element
 * @param {any = ''} layoutPointer - the layoutPointer to layout, inside rootLayout
 * @param {any[] = layout} rootLayout - the root layout, which conatins layout
 * @return {[type]}
 */
export function mapLayout(
  layout: any[],
  fn: (v: any, i?: number, p?: string, l?: any) => any,
  layoutPointer: string = '',
  rootLayout: any[] = layout
): any[] {
  let indexPad: number = 0;
  let newLayout: any[] = [];
  forEach(layout, (item, index) => {
    let realIndex = +index + indexPad;
    let newLayoutPointer = layoutPointer + '/' + realIndex;
    let newNode: any = copy(item);
    let itemsArray: any[] = [];
    if (isObject(item)) {
      if (hasOwn(item, 'tabs')) {
        item.items = item.tabs;
        delete item.tabs;
      }
      if (hasOwn(item, 'items')) {
        itemsArray = isArray(item.items) ? item.items : [item.items];
      }
    }
    if (itemsArray.length) {
      newNode.items = mapLayout(itemsArray, fn, newLayoutPointer + '/items', rootLayout);
    }
    newNode = fn(newNode, realIndex, newLayoutPointer, rootLayout);
    if (!isDefined(newNode)) {
      indexPad--;
    } else {
      if (isArray(newNode)) { indexPad += newNode.length - 1; }
      newLayout = newLayout.concat(newNode);
    }
  });
  return newLayout;
};

/**
 * 'buildTitleMap' function
 *
 * @param {any} titleMap -
 * @param {any} enumList -
 * @param {boolean = false} fieldRequired -
 * @return {{name: string, value: any}[]}
 */
export function buildTitleMap(
  titleMap: any, enumList: any, fieldRequired: boolean = true
): { name: string, value: any }[] {
  let newTitleMap: { name: string, value: any }[] = [];
  let hasEmptyValue: boolean = false;
  if (titleMap) {
    if (isArray(titleMap)) {
      if (enumList) {
        for (let i of Object.keys(titleMap)) {
          if (isObject(titleMap[i])) { // JSON Form / Angular Schema Form style
            const value: any = titleMap[i].value;
            if (enumList.indexOf(value) !== -1) {
              const name: string = titleMap[i].name;
              newTitleMap.push({ name, value });
              if (!value) { hasEmptyValue = true; }
            }
          } else if (isString(titleMap[i])) { // React Jsonschema Form style
            if (i < enumList.length) {
              const name: string = titleMap[i];
              const value: any = enumList[i];
              newTitleMap.push({ name, value });
              if (!value) { hasEmptyValue = true; }
            }
          }
        }
      } else { // If array titleMap and no enum list, just return the titleMap
        newTitleMap = titleMap;
        if (!fieldRequired) { hasEmptyValue = !!newTitleMap.filter(i => !i.value).length; }
      }
    } else if (enumList) { // Alternate JSON Form style, with enum list
      for (let i of Object.keys(enumList)) {
        let value: any = enumList[i];
        if (hasOwn(titleMap, value)) {
          let name: string = titleMap[value];
          newTitleMap.push({ name, value });
          if (!value) { hasEmptyValue = true; }
        }
      }
    } else { // Alternate JSON Form style, without enum list
      for (let value of Object.keys(titleMap)) {
        let name: string = titleMap[value];
        newTitleMap.push({ name, value });
        if (!value) { hasEmptyValue = true; }
      }
    }
  } else if (enumList) { // Build map from enum list alone
    for (let i of Object.keys(enumList)) {
      let name: string = enumList[i];
      let value: any = enumList[i];
      newTitleMap.push({ name, value});
      if (!value) { hasEmptyValue = true; }
    }
  } else { // If no titleMap and no enum list, return default map of boolean values
    newTitleMap = [{ name: 'True', value: true }, { name: 'False', value: false }];
  }
  if (!fieldRequired && !hasEmptyValue) {
    newTitleMap.unshift({ name: '', value: '' });
  }
  return newTitleMap;
}
