Name

sn_flow_diagram.FlowDiagramBuilderApi

Description

No description available

Script

var FlowDiagramBuilderApi = Class.create();
FlowDiagramBuilderApi.prototype = {
  initialize: function() {},
  type: 'FlowDiagramBuilderApi'
};
// class variables
FlowDiagramBuilderApi.templateDiagramJson = null;
FlowDiagramBuilderApi.linkLabel = "";
FlowDiagramBuilderApi.edgeId = "";
FlowDiagramBuilderApi.startPort = "";
FlowDiagramBuilderApi.endPort = "";
FlowDiagramBuilderApi.startKey = "";
FlowDiagramBuilderApi.endKey = "";
FlowDiagramBuilderApi.addBtnVisibility = "onHover";
FlowDiagramBuilderApi.addBtnText = "";
FlowDiagramBuilderApi.flowLogicRecordsHash = {};
FlowDiagramBuilderApi.recordOrder = 0;
FlowDiagramBuilderApi.instanceRecordsHash = {};
FlowDiagramBuilderApi.instanceRecordsDepthHash = {};
FlowDiagramBuilderApi.instanceRecordsOrderedList = [];
FlowDiagramBuilderApi.sourceIfsByDepthHash = {};
FlowDiagramBuilderApi.maxInteger = 4503599627370495;
FlowDiagramBuilderApi.currentParallelBlockKey = "";
FlowDiagramBuilderApi.currentParallelBlockDepth = -1;
FlowDiagramBuilderApi.currentDecisionBlockKey = "";
FlowDiagramBuilderApi.currentDecisionBlockDepth = -1;
FlowDiagramBuilderApi.parallelBranchAddLookup = {};
FlowDiagramBuilderApi.stagesAssociatedNodeMap = {};
FlowDiagramBuilderApi.decisionAnswers = {};
// hash of BEGINNING and END records
FlowDiagramBuilderApi.hiddenRecordsHash = {};
FlowDiagramBuilderApi.previousIfOrElseIfKey = "";
// maps deleted else node to the node that replaces the else node
FlowDiagramBuilderApi.elseReplacementHash = {};
FlowDiagramBuilderApi.getTranslatedFlowFromDatabase = function(flowSysId, errors) {
  if (!errors || !(errors instanceof Array))
      errors = [];
  var flow = sn_flow.FlowDesigner.getCompleteFlow(flowSysId);
  return FlowDiagramBuilderApi.getTranslatedFlow(flow, errors);
};

FlowDiagramBuilderApi.populateSortedInstances = function(flowJSON, instances) {
  // flow from UI only has componentInstances; flow from database only has actionInstances, flowLogicInstances, and/or subflowInstances
  if (flowJSON.componentInstances)
      instances = FlowDiagramBuilderApi.concatToMainArrayWithFlowDiagramType(instances, flowJSON.componentInstances, FlowDiagramConstants.INSTANCE_TYPES.COMPONENT_INSTANCE_TYPE);
  else {
      if (flowJSON.actionInstances)
          instances = FlowDiagramBuilderApi.concatToMainArrayWithFlowDiagramType(instances, flowJSON.actionInstances, FlowDiagramConstants.INSTANCE_TYPES.ACTION_INSTANCE_TYPE);
      if (flowJSON.flowLogicInstances)
          instances = FlowDiagramBuilderApi.concatToMainArrayWithFlowDiagramType(instances, flowJSON.flowLogicInstances, FlowDiagramConstants.INSTANCE_TYPES.FLOW_LOGIC_INSTANCE_TYPE);
      if (flowJSON.subFlowInstances)
          instances = FlowDiagramBuilderApi.concatToMainArrayWithFlowDiagramType(instances, flowJSON.subFlowInstances, FlowDiagramConstants.INSTANCE_TYPES.SUBFLOW_INSTANCE_TYPE);
  }
  // this sort supports order value with dots (e.g. 1.2.33)
  instances = FlowDiagramHelper.sortByArrayPosition(instances);
  return instances;
};

// flow can be full fledged flow from database or UI or in the following structure:
// {
//     name,
//     id,   // sys_if of the flow
//     type, // flow,subflow,action
//     componentInstances: [
//         {
//             id,
//             uiUniqueIdentifier,
//             name,
//             parent,
//             order,
//             arrayPosition,
//             connectedTo,
//             depth,
//             actionType: {
//                 parent_action,
//                 id,
//                 name
//             },
//             flowLogicDefinition: {
//                 id,
//                 type,
//                 name
//             }
//         }
//     ]
// }
FlowDiagramBuilderApi.getTranslatedFlow = function(flowJSON, errors) {
  if (!errors || !(errors instanceof Array))
      errors = [];
  var dataModel = FlowDiagramBuilderApi.getDiagramJSON(flowJSON, errors);
  dataModel.configuration = FlowDiagramBuilderApi.getConfiguration();
  return dataModel;
};

FlowDiagramBuilderApi.getDiagramJSON = function(flowJSON, errors) {
  if (!errors || !(errors instanceof Array))
      errors = [];
  var i, j;
  var dataModel = FlowDiagramTranslateModel.getDataModel();
  dataModel.diagramJSON = FlowDiagramTranslateModel.getDiagramJSONModel();

  if (flowJSON) {
      if (flowJSON && errors.length === 0) {
          dataModel.name = flowJSON.name;
          dataModel.flowIsReadOnly = flowJSON.flowIsReadOnly;

          try {
              var templateJSON = FlowDiagramDatabaseApi.getTemplateJSON(FlowDiagramConstants.SYS_IDS.SN_FLOW_DIAGRAM_BUILDER_CONFIGURATION);
              FlowDiagramBuilderApi.templateDiagramJson = JSON.parse(templateJSON);
          } catch (e) {
              gs.error("FlowDiagramBuilderApi: " + e);
              errors.push(e);
          }

          if (errors.length === 0 && FlowDiagramBuilderApi.templateDiagramJson && FlowDiagramBuilderApi.templateDiagramJson.edges.length > 0) {
              FlowDiagramBuilderApi.edgeId = FlowDiagramBuilderApi.templateDiagramJson.edges[0].edge_id;
              FlowDiagramBuilderApi.startPort = FlowDiagramBuilderApi.templateDiagramJson.edges[0].fromPort;
              FlowDiagramBuilderApi.endPort = FlowDiagramBuilderApi.templateDiagramJson.edges[0].toPort;
          }

          if (errors.length === 0) {
              var existingTrigger = false;
              var existingSubflowInputs = flowJSON.existingSubflowInputs;
              var triggerInstanceName = FlowDiagramConstants.NODE_NAMES.TRIGGER;
              if (flowJSON.triggerInstances && flowJSON.triggerInstances.length > 0) {
                  existingTrigger = true;
                  triggerInstanceName = flowJSON.triggerInstances[0].name;
              }
              var hierarchyStack = [];
              var flowType = flowJSON.type;
              var existingSubflowInputs = flowJSON.existingSubflowInputs;
              var error = FlowDiagramBuilderApi.addStartEnd(hierarchyStack, dataModel, triggerInstanceName, existingTrigger, flowType, existingSubflowInputs);
              if (error)
                  errors.push(error);
              else {
                  var instances = [];
                  try {
                      instances = FlowDiagramBuilderApi.populateSortedInstances(flowJSON, instances);
                      var activeIfKeyByDepthHash = {};
                      for (i = 0; i < instances.length; i++) {
                          FlowDiagramBuilderApi.addInstance(errors, hierarchyStack, dataModel, instances[i], activeIfKeyByDepthHash);
                      }

                      FlowDiagramBuilderApi.processRemainingActiveIfKeys(activeIfKeyByDepthHash, dataModel.diagramJSON.nodes);
                      FlowDiagramBuilderApi.updateEndOfParallelAndParallelBlock(dataModel.diagramJSON.nodes);
                      FlowDiagramBuilderApi.updatePathBroken(errors, dataModel.diagramJSON.nodes, dataModel.diagramJSON.edges);
                      // setupLoopExitNodeHashAndloopBackNodes will calculate all IF nodes and FOR loop nodes that ends with a node
                      // then based on their depth, END IF or END LOOP nodes will be created and added before the original node.
                      // If a END node was created, all edges pointing to the original node prior to this will be updated to point to the corresponding
                      // END node. All loopback edges created prior to this will be deleted. A new loopback edge will be created if END LOOP node
                      // was created and the loopback edge will be sourced from the END LOOP node.
                      var loopExitNodeHash = FlowDiagramBuilderApi.setupLoopExitNodeHashAndloopBackNodes(dataModel.diagramJSON.nodes, dataModel.diagramJSON.edges);
                      dataModel.diagramJSON.nodes = FlowDiagramBuilderApi.processEndNodes(dataModel.diagramJSON.nodes, dataModel.diagramJSON.edges, loopExitNodeHash);
                      // has to execute after end of parallel node is generated
                      FlowDiagramBuilderApi.updatePathBrokenForParallel(dataModel.diagramJSON.nodes, dataModel.diagramJSON.edges);
                      FlowDiagramBuilderApi.removeImmediateEndNodesConnectedToPathBroken(dataModel.diagramJSON.nodes, dataModel.diagramJSON.edges);
                      FlowDiagramBuilderApi.processOrphanedEndOfNodeInPathBroken(dataModel.diagramJSON.nodes, dataModel.diagramJSON.edges);
                      FlowDiagramBuilderApi.connectPathBrokenToNonStartPathBrokenNodeWithoutIncomingEdges(dataModel.diagramJSON.nodes, dataModel.diagramJSON.edges);

                      FlowDiagramBuilderApi.updateGoBackToEdges(dataModel.diagramJSON.nodes, dataModel.diagramJSON.edges);
                      FlowDiagramBuilderApi.updateEdgeLabels(dataModel.diagramJSON.edges);
                      FlowDiagramBuilderApi.removeUnnecessaryEdges(dataModel.diagramJSON.edges);
                      FlowDiagramBuilderApi.removeAddButtonOnEndEdges(dataModel.diagramJSON.edges);
                      FlowDiagramBuilderApi.removeUnconnectedNodes(dataModel.diagramJSON.nodes, dataModel.diagramJSON.edges);
                      FlowDiagramBuilderApi.addStageNodes(flowJSON, dataModel.diagramJSON.nodes, dataModel.diagramJSON.edges);
                      FlowDiagramBuilderApi.removeUnnecessaryFieldsOnEdges(dataModel.diagramJSON.edges);
                      FlowDiagramBuilderApi.removeUnnecessaryFieldsOnNodes(dataModel.diagramJSON.nodes);
                      FlowDiagramBuilderApi.assignSortedNodesToDataModel(dataModel);
                  } catch (e) {
                      errors.push(e);
                  }
              }
          }
          // Todo:
          // The following two methods loop through all the nodes and edges
          // We should combine them into the construction code of the nodes and edges to improve performance.
          var nodeMap = {};
          try {
              nodeMap = FlowDiagramBuilderApi.addDataModelRelationship(flowJSON, dataModel);
          } catch (e) {
              errors.push(e);
          }
          try {
              FlowDiagramBuilderApi.decorateLinks(dataModel, nodeMap);
          } catch (e) {
              errors.push(e);
          }
      }
  }
  return dataModel;
};

FlowDiagramBuilderApi.updateGoBackToEdges = function(nodes, edges) {
  var goBackToLabelMap = {};
  var updateEdgeLabels = [];
  var deleteEdges = {};
  var fromNodes = nodes.filter(function(n) {
      return n.node_id === FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_GO_BACK_TO_ACTION;
  });

  for (var i = 0; i < fromNodes.length; i++) {
      var goBackToNode = fromNodes[i];
      var goBackToTargetKey = goBackToNode.data.go_back_to_step;
      var toNode = FlowDiagramBuilderApi.instanceRecordsHash[goBackToTargetKey];

      if (toNode && toNode.key) {
          var goBackToEdge = edges.filter(function(e) {
              return e.from === goBackToNode.key;
          })[0];
          if (goBackToEdge) {
              var toNodeHash = FlowDiagramBuilderApi.instanceRecordsHash[toNode.key];
              var toPort = FlowDiagramBuilderApi.getToPort(toNodeHash.type);
              goBackToEdge.to = toNode.key;
              goBackToEdge.toPort = toPort;
              goBackToEdge.linkLabel = "";

              var currentLabel = "";
              if (goBackToLabelMap[goBackToTargetKey]) {
                  currentLabel = goBackToLabelMap[goBackToTargetKey];
              }
              var label = FlowDiagramHelper.getGoBackToLabel(goBackToTargetKey, goBackToNode.order, currentLabel);
              goBackToLabelMap[goBackToTargetKey] = label;
              updateEdgeLabels.push({
                  key: goBackToTargetKey,
                  edge: goBackToEdge
              });
          }
      } else if (!goBackToTargetKey) { // go_back_to_step is empty, no toNode, to delete the edge
          deleteEdges[goBackToNode.key] = goBackToNode.key;
      }
  }

  // update labels on GoBackTo edges
  for (var j = 0; j < updateEdgeLabels.length; j++) {
      var targetKey = updateEdgeLabels[j].key;
      var edge = updateEdgeLabels[j].edge;
      edge.linkLabel = goBackToLabelMap[targetKey];
  }

  // remove edges
  for (var i = 0; i < edges.length; i++) {
      // Do not delete GoBackTo edge as we will update later
      if (deleteEdges[edges[i].from]) {
          edges.splice(i, 1);
          i--;
      }
  }
};

// Adds stage nodes. Updates existing network of nodes and edges accordingly.
FlowDiagramBuilderApi.addStageNodes = function(flowJSON, totalNodes, totalEdges) {
  var i, j, k;
  if (flowJSON && flowJSON.stages) {
      var stages = [];
      // Sort edges based on its destination (to)
      var edgesByTo = {};
      for (i = 0; i < totalEdges.length; i++) {
          var edge = totalEdges[i];
          if (edge.to) {
              if (!edgesByTo[edge.to]) {
                  edgesByTo[edge.to] = [];
              }
              edgesByTo[edge.to].push(edge);
          }
      }
      for (var key in flowJSON.stages) {
          if (Object.prototype.hasOwnProperty.call(flowJSON.stages, key)) {
              var stage = flowJSON.stages[key];
              if (stage && stage.componentIndexes) {
                  for (i = 0; i < stage.componentIndexes.length; i++) {
                      var componentIndexValue = stage.componentIndexes[i];
                      // create the stage node for each stage.componentIndexes
                      var stageNode = {
                          key: FlowDiagramHelper.addUiUniqueIdentifierDashes(sn_flow.FlowDesigner.getGeneratedGuid()),
                          instanceType: FlowDiagramConstants.INSTANCE_TYPES.STAGE_TYPE,
                          type: stage.type,
                          label: stage.label,
                          instance_sys_id: stage.id,
                          stageId: stage.stageId,
                          node_id: FlowDiagramConstants.SYS_IDS.FLOW_STAGE_ACTION,
                          componentIndexes_index: i,
                          componentIndexes_value: componentIndexValue,
                          arrayPosition: componentIndexValue,
                          associated_node_key: "", // will be added later
                          order: stage.order,
                          duration: stage.duration,
                          value: stage.value,
                          definition_sys_id: "", // will be empty
                          depth: "", // will be added later
                          data: {
                              stageLabel: stage.label,
                              stageDurationLabel: "",
                              isLabelBold: stage.type !== "error",
                              isDurationBold: stage.type === "error",
                              isErrorStage: stage.type === "error",
                          }
                      };
                      stages.push(stageNode);
                  }
              }
          }
      }
      var orderedStages = FlowDiagramHelper.sortByArrayPosition(stages, "desc");
      var orderedNodes = [];
      for (i = 0; i < totalNodes.length; i++) {
          if (totalNodes[i].arrayPosition || totalNodes[i].arrayPosition === 0) {
              orderedNodes.push(totalNodes[i]);
          }
      }
      orderedNodes = FlowDiagramHelper.sortByArrayPosition(orderedNodes, "desc");
      for (i = 0; i < orderedStages.length; i++) {
          var currentStage = orderedStages[i];
          var validStagePositionCount = 0;
          // stage arrayPosition can be iterated not in order
          for (j = 0; j < orderedNodes.length; j++) {
              var currentNode = orderedNodes[j];
              var isCurrentNodeNotUnderForEach = currentNode && currentNode.sourceForEachKeys && currentNode.sourceForEachKeys instanceof Array && (currentNode.sourceForEachKeys.toString().includes(currentNode.key) || currentNode.sourceForEachKeys.length === 0);
              var currentNodeHasCurrentStage = currentNode && (currentNode.arrayPosition || currentNode.arrayPosition === 0) && currentNode.arrayPosition === currentStage.arrayPosition;
              if (currentNodeHasCurrentStage && isCurrentNodeNotUnderForEach && !currentNode.parallelBlockKey) {
                  // stage cannot be within a FOR loop nor PARALLEL FOR
                  // fill in the stage node's remaining fields after finding the stage's attached node
                  currentStage.depth = currentNode.depth;
                  currentStage.associated_node_key = currentNode.key;
                  currentStage.key = FlowDiagramHelper.addStringToUuid(FlowDiagramConstants.INSTANCE_TYPES.STAGE_TYPE, currentNode.key);
                  // store map of stage key to associated node key for lookup
                  FlowDiagramBuilderApi.stagesAssociatedNodeMap[currentStage.key] = currentNode.key;
                  if (currentNode.parent) {
                      currentStage.parent = currentNode.parent;
                  }
                  // find all edges pointing to the stage's attached node and update the edges to point to the stage node
                  var connectedEdges = edgesByTo[currentNode.key];
                  if (connectedEdges) {
                      for (k = 0; k < connectedEdges.length; k++) {
                          var connectedEdge = connectedEdges[k];
                          if (connectedEdge.toPort !== FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID) {
                              connectedEdge.to = currentStage.key;
                              connectedEdge.toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_STAGE_INPUT_SYS_ID;
                          }
                      }
                  }
                  // add a new edge that will connect between the stage node and stage's attached node
                  var newEdge = FlowDiagramTranslateModel.getEdgeModel();
                  if (newEdge && FlowDiagramBuilderApi.instanceRecordsHash[currentNode.key]) {
                      newEdge = FlowDiagramBuilderApi.updateEdge(
                          newEdge, {
                              from: currentStage.key,
                              to: currentNode.key,
                              fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_STAGE_OUTPUT_SYS_ID,
                              toPort: FlowDiagramBuilderApi.getToPort(FlowDiagramBuilderApi.instanceRecordsHash[currentNode.key].type),
                              edgeId: FlowDiagramBuilderApi.edgeId,
                              addBtnVisibility: FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopLoopback, // remove the add button
                              hideToArrow: true
                          }
                      );
                      totalEdges.push(newEdge);
                  }
                  validStagePositionCount++;
                  break;
              }
          }
          delete currentStage.arrayPosition;
          // only add the stage if there is one or more valid stage placement in flow
          if (validStagePositionCount > 0) {
              totalNodes.push(currentStage);
          }
      }
  }
};

// there could be non-path broken node that will need to be connected to a path broken node
// this node will not have any incoming edge
// only path broken node can have no incoming edges
// path broken node created in this function will not have an associated END OF node
FlowDiagramBuilderApi.connectPathBrokenToNonStartPathBrokenNodeWithoutIncomingEdges = function(nodes, edges) {
  var edgesTo = {};
  var edgesFrom = {};
  var orderedNodes = FlowDiagramBuilderApi.sortNodes(nodes);
  var i;
  for (i = 0; i < edges.length; i++) {
      var edge = edges[i];
      edgesTo[edge.to] = true;
  }
  for (i = 0; i < orderedNodes.length; i++) {
      var node = orderedNodes[i];
      // create path broken node if the node is not path broken nor start nor trigger node nor end of * node
      // and there are no incoming edges
      if (!((node.instanceType &&
                  (node.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PATHBROKEN ||
                      node.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDIF ||
                      node.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP ||
                      node.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP ||
                      node.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDPARALLEL ||
                      node.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDECISION
                  )) ||
              node.key === FlowDiagramBuilderApi.startKey ||
              node.key === FlowDiagramConstants.NODE_KEYS.FLOW_DIAGRAM_ADD_TRIGGER_NODE_KEY) &&
          !edgesTo[node.key]) {

          var nodeId = FlowDiagramBuilderApi.getFlowLogicNodeId(FlowDiagramConstants.FLOW_LOGIC_TYPES.PATHBROKEN);
          // PATH BROKEN node is not defined anywhere in Flow Designer. We could have used END Node's key as PATH BROKEN node's
          // key but decided to create its own since all node keys should be unique
          var newKey = FlowDiagramHelper.addUiUniqueIdentifierDashes(sn_flow.FlowDesigner.getGeneratedGuid());
          // PATH BROKEN node does not exist in Flow and therefore there is no instance.order for it
          var newNode = FlowDiagramBuilderApi.createNode({
              name: FlowDiagramConstants.NODE_NAMES.PATH_BROKEN,
              key: newKey,
              node_id: nodeId,
              instanceType: FlowDiagramConstants.FLOW_LOGIC_TYPES.PATHBROKEN
          });
          nodes.push(newNode);

          var newEdge = FlowDiagramTranslateModel.getEdgeModel();
          if (newEdge) {
              newEdge = FlowDiagramBuilderApi.updateEdge(
                  newEdge, {
                      from: newNode.key,
                      to: node.key,
                      fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_PATH_BROKEN_OUTPUT_SYS_ID,
                      toPort: FlowDiagramBuilderApi.getToPort(node.type),
                      edgeId: FlowDiagramBuilderApi.edgeId,
                  }
              );

              edges.push(newEdge);
          }
      }
  }
};

FlowDiagramBuilderApi.updateEdgesFromToPointToNewNode = function(edges, node, newNode, fromPort) {
  for (var i = 0; i < edges.length; i++) {
      var edge = edges[i];
      if (edge.from === node.key) {
          edge.from = newNode.key;
          edge.fromPort = fromPort;
      }
  }
};

// first/immediate END OF node attached to Path Broken node is not necessary because you cannot
// add a node after END flow logic node. Therefore remove the immediate END OF node followed by a
// path broken node
// there could be Path Broken node added without assoicated END OF node by
// connectPathBrokenToNonStartPathBrokenNodeWithoutIncomingEdges()
FlowDiagramBuilderApi.removeImmediateEndNodesConnectedToPathBroken = function(nodes, edges) {
  var i, j;
  var orderedNodes = FlowDiagramBuilderApi.sortNodes(nodes, "desc");
  var nodeTypeFrom;
  // record all path broken nodes and edges from the path broken node in pathBrokenNodes
  var pathBrokenNodes = {};
  for (i = 0; i < edges.length; i++) {
      var edge = edges[i];
      nodeTypeFrom = FlowDiagramBuilderApi.getInstanceRecordHashNodeType(edge.from);
      if (nodeTypeFrom && nodeTypeFrom === FlowDiagramConstants.FLOW_LOGIC_TYPES.PATHBROKEN) {
          // nodeTypeTo is node type of first immediate node attached to this path broken node
          var nodeTypeTo = FlowDiagramBuilderApi.getHiddenRecordHashNodeType(edge.to);
          if (nodeTypeTo && (nodeTypeTo === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP ||
                  nodeTypeTo === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP ||
                  nodeTypeTo === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDIF ||
                  nodeTypeTo === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDPARALLEL || nodeTypeTo === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDECISION)) {
              pathBrokenNodes[edge.from] = {
                  endNode: FlowDiagramBuilderApi.hiddenRecordsHash[edge.to],
                  edgeFromPathBroken: edge,
                  edgesFromEnd: []
              };
              for (j = 0; j < edges.length; j++) {
                  // if nodeTypeTo exists then FlowDiagramBuilderApi.hiddenRecordsHash[edge.to] exists
                  if (edges[j].from === FlowDiagramBuilderApi.hiddenRecordsHash[edge.to].key) {
                      var edgeFromNodeTypeTo = edges[j];
                      pathBrokenNodes[edge.from].edgesFromEnd.push(edgeFromNodeTypeTo);
                  }
              }
          }
      }
  }

  var edgesToDelete = {};
  var nodesToDelete = {};
  for (i = 0; i < orderedNodes.length; i++) {
      var node = orderedNodes[i];
      // skip parallel path broken nodes as they are handled within updatePathBrokenForParallel()
      if (pathBrokenNodes[node.key] && !node.endParallelNodeKey && !node.endDecisionNodeKey) {
          // update all edges from this particular path broken node in pbNode
          var pbNode = pathBrokenNodes[node.key];
          if (pbNode.endNode && pbNode.edgeFromPathBroken && pbNode.edgesFromEnd) {
              var edgeFrom = pbNode.edgeFromPathBroken;
              for (j = 0; j < pbNode.edgesFromEnd.length; j++) {
                  var edgeTo = pbNode.edgesFromEnd[j];
                  edgeTo.from = edgeFrom.from;
                  edgeTo.fromPort = edgeFrom.fromPort;
              }

              // copy END OF field from the deleted END OF node to the path broken
              // this copy will enable the path broken node to function as the
              // END OF node deleted in UI
              if (pbNode.endNode.loopStartNodeKey) {
                  node.loopStartNodeKey = pbNode.endNode.loopStartNodeKey;
              } else if (pbNode.endNode.sourceIfNodeKey) {
                  node.sourceIfNodeKey = pbNode.endNode.sourceIfNodeKey;
              } else if (pbNode.endNode.endParallelNodeKey) {
                  node.endParallelNodeKey = pbNode.endNode.endParallelNodeKey;
              } else if (pbNode.endNode.endDecisionNodeKey) {
                  node.endDecisionNodeKey = pbNode.endNode.endDecisionNodeKey;
              }

              edgesToDelete[edgeFrom.to] = edgeFrom;
              nodesToDelete[pbNode.endNode.key] = pbNode.endNode;
          }
      }
  }
  FlowDiagramBuilderApi.deleteNodesIfKeyMatch(nodes, nodesToDelete);
  FlowDiagramBuilderApi.deleteEdgesIfToMatch(edges, edgesToDelete);
};

FlowDiagramBuilderApi.updateRecordsToDelete = function(node, edgesFrom, edgesTo, edgesToDelete, nodesToDelete) {
  if (node, edgesFrom, edgesTo, edgesToDelete, nodesToDelete) {
      var edgeFrom = edgesFrom[node.key];
      var edgeTo = edgesTo[node.key];
      if (edgeFrom && edgeTo) {
          // update edgeFrom, delete edgeTo, delete node
          edgeFrom.from = edgeTo.from;
          edgeFrom.fromPort = edgeTo.fromPort;
          edgesTo[node.key] = null;
          edgesToDelete[edgeTo.to] = edgeTo;
          nodesToDelete[node.key] = node;
      }
  }
};

FlowDiagramBuilderApi.deleteNodesIfKeyMatch = function(nodes, nodesToDelete) {
  for (var i = 0; i < nodes.length; i++) {
      if (nodesToDelete[nodes[i].key]) {
          nodes.splice(i, 1);
          i--;
      }
  }
};

FlowDiagramBuilderApi.deleteEdgesIfToMatch = function(edges, edgesToDelete) {
  for (var i = 0; i < edges.length; i++) {
      // Do not delete GoBackTo edge as we will update later
      if (edgesToDelete[edges[i].to] && FlowDiagramBuilderApi.instanceRecordsHash[edges[i].from].type !== FlowDiagramConstants.FLOW_LOGIC_TYPES.GOBACKTO) {
          edges.splice(i, 1);
          i--;
      }
  }
};

FlowDiagramBuilderApi.getInstanceRecordHashNodeType = function(key) {
  if (key &&
      FlowDiagramBuilderApi.instanceRecordsHash[key] &&
      FlowDiagramBuilderApi.instanceRecordsHash[key].type) {
      return FlowDiagramBuilderApi.instanceRecordsHash[key].type;
  } else {
      return null;
  }
};

FlowDiagramBuilderApi.getHiddenRecordHashNodeType = function(key) {
  if (key &&
      FlowDiagramBuilderApi.hiddenRecordsHash[key] &&
      FlowDiagramBuilderApi.hiddenRecordsHash[key].instanceType) {
      return FlowDiagramBuilderApi.hiddenRecordsHash[key].instanceType;
  } else {
      return null;
  }
};

FlowDiagramBuilderApi.getSourceIfKeysAsText = function(nodeKey) {
  var sourceIfKeyText = "";
  var sourceIfKeys = FlowDiagramBuilderApi.getSourceIfKeysFromNodeKey(nodeKey);
  if (sourceIfKeys && sourceIfKeys instanceof Array && sourceIfKeys.length > 0) {
      sourceIfKeyText = sourceIfKeys.join(',');
  }
  return sourceIfKeyText;
};

FlowDiagramBuilderApi.updateInstanceRecordHash = function(nodeKey, key, value) {
  if (nodeKey && FlowDiagramBuilderApi.instanceRecordsHash[nodeKey]) {
      FlowDiagramBuilderApi.instanceRecordsHash[nodeKey][key] = value;
  }
};

FlowDiagramBuilderApi.isElseBlock = function(previousNode, nextNode, sourceNode, nodeType) {
  // if previousNode and nextNode have match sourceIfKeys
  if (sourceNode && previousNode && previousNode.sourceIfKeys && nextNode.sourceIfKeys) {
      if (previousNode.sourceIfKeys.toString().includes(sourceNode.key) && nextNode.sourceIfKeys.toString().includes(sourceNode.key)) {
          if ((previousNode.depth > nextNode.depth || nextNode.depth === (sourceNode.depth + 1)) && nextNode.arrayPosition > (previousNode.arrayPosition + 1)) {
              return true;
          } else if (nodeType && nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF) {
              return true;
          }
      }
  }
  return false;
};

FlowDiagramBuilderApi.pushEndOfLogicKeys = function(node, keys, type) {
  if (node) {
      if (!node[type]) {
          node[type] = [];
      }
      if (typeof keys === "string") {
          node[type].push(keys);
      }
      if (keys instanceof Array) {
          for (var i = 0; i < keys.length; i++) {
              node[type].push(keys[i].key);
          }
      }
  }
};

FlowDiagramBuilderApi.pushEndOfParallelKeys = function(node, keys) {
  FlowDiagramBuilderApi.pushEndOfLogicKeys(node, keys, "endOfParallelKeys");
};

FlowDiagramBuilderApi.pushEndOfDecisionKeys = function(node, keys) {
  FlowDiagramBuilderApi.pushEndOfLogicKeys(node, keys, "endOfDecisionKeys");
};

FlowDiagramBuilderApi.removeFromArrayByKey = function(array, key, value) {
  if (!array || !(array instanceof Array) || !key || !value) {
      return;
  }
  for (var i = 0; i < array.length; i++) {
      if (array[i][key] === value) {
          array.splice(i, 1);
          break;
      }
  }
};

// Description: this method sets two attributes on nodes: parallelKey/decisioinoKey and endOfParallelKeys/endOfDecisionKeys
//      parallelKey         set when a node is identified as being within a PARALLEL block
//      decisionKey         set when a node is identified as being within a DECISION block
//      endOfParallelKeys   set when a node is identified as the node which ENDPARALLEL will connect to
//      endOfDecisionKeys   set when a node is identified as the node which ENDDECISION will connect to
FlowDiagramBuilderApi.updateEndOfParallelAndParallelBlock = function(nodes) {
  // this value is [] where is there is no ancestor PARALLEL/DECISION. Can have multiple current parallel/decision keys
  var foundParallelNodes = [];
  var foundParallelNodesMap = {};
  var currentParallelNode;
  var parallelNodeActiveByDecision;

  var foundDecisionNodes = [];
  var foundDecisionNodesMap = {};
  var currentDecisionNode;
  var decisionNodeActiveByParallel;

  var previousNodeType;
  var orderedNodes = FlowDiagramBuilderApi.sortNodes(nodes);
  var i, j, k;
  for (i = 0; i < orderedNodes.length; i++) {
      var currentNode = orderedNodes[i];
      var previousNode;
      if (i > 0) {
          previousNode = orderedNodes[i - 1];
      }

      // only process nodes that are not Trigger or Add a node (ie have an arrayPosition)
      if (currentNode && FlowDiagramBuilderApi.isValidArrayPosition(currentNode.arrayPosition)) {
          var nodeType = FlowDiagramBuilderApi.getInstanceRecordHashNodeType(currentNode.key);
          // Note: sourceKey can be - sourceForEachKeys, sourceIfKeys, parallelKey, decisionKey
          var sourceIfNode = FlowDiagramBuilderApi.getImmediateSourceIfKeyNodeFromNode(currentNode);

          var processedParallelResult = FlowDiagramBuilderApi.processParallel(nodeType, currentNode, previousNode, sourceIfNode, parallelNodeActiveByDecision, currentParallelNode, foundParallelNodes, foundParallelNodesMap);
          parallelNodeActiveByDecision = processedParallelResult.parallelNodeActiveByDecision;
          currentParallelNode = processedParallelResult.currentParallelNode;
          var processedDecisionResult = FlowDiagramBuilderApi.processDecision(nodeType, currentNode, previousNode, sourceIfNode, decisionNodeActiveByParallel, currentDecisionNode, foundDecisionNodes, foundDecisionNodesMap);
          decisionNodeActiveByParallel = processedDecisionResult.decisionNodeActiveByParallel;
          currentDecisionNode = processedDecisionResult.currentDecisionNode;

      } else if (currentNode && currentNode.key === FlowDiagramBuilderApi.endKey) {
          if (foundParallelNodes.length > 0) {
              // we have reached last node "Add a node" signifying end of PARALLEL
              FlowDiagramBuilderApi.pushEndOfParallelKeys(currentNode, foundParallelNodes);
          }
          if (foundDecisionNodes.length > 0) {
              // we have reached last node "Add a node" signifying end of DECISION
              FlowDiagramBuilderApi.pushEndOfDecisionKeys(currentNode, foundDecisionNodes);
          }
      }
  }
};

FlowDiagramBuilderApi.processParallel = function(nodeType, currentNode, previousNode, sourceIfNode, parallelNodeActiveByDecision, currentParallelNode, foundParallelNodes, foundParallelNodesMap) {
  // 0. if parallelNodeActiveByDecision then there is an active parallel logic that has not ended because of
  // make-a-decision flow logic. If the current node is not part a DECISION BLOCK logic, then claim the
  // end of the active parallel logic. Another way for the active parallel logic to end with
  // parallelNodeActiveByDecision (having a value) is reaching the end node, which will claim
  // end of all active parallel logic.
  var parallelNode;
  var previousNodeType;
  if (parallelNodeActiveByDecision && !(nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK || currentNode.decisionBlockKey)) {
      parallelNode = parallelNodeActiveByDecision;
      parallelNodeActiveByDecision = null;
      FlowDiagramBuilderApi.pushEndOfParallelKeys(currentNode, parallelNode.key);
      if (currentParallelNode && currentParallelNode.key === parallelNode.key) {
          currentParallelNode = null;
      }
      // remove from foundParallelNodes
      for (k = 0; k < foundParallelNodes.length; k++) {
          FlowDiagramBuilderApi.removeFromArrayByKey(foundParallelNodes, "key", parallelNode.key);
      }
  }

  // 1. set the parallelKey attribute
  if (nodeType && (nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLEL)) {
      currentParallelNode = currentNode;
      foundParallelNodesMap[currentNode.key] = currentNode;
      foundParallelNodes.push({
          key: currentNode.key
      });
      currentNode.parallelKey = currentNode.key;
      FlowDiagramBuilderApi.updateInstanceRecordHash(currentNode.key, "parallelKey", currentNode.key);
  } else if (currentParallelNode && (currentNode.depth > currentParallelNode.depth || currentNode.parallelBlockKey || (nodeType && nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK))) {
      // is a child of PARALLEL node
      currentNode.parallelKey = currentParallelNode.key;
      FlowDiagramBuilderApi.updateInstanceRecordHash(currentNode.key, "parallelKey", currentParallelNode.key);
  }

  // 2. set the endOfParallelKeys attribute on first node outside of PARALLEL block, or set flag endOfIfKey if we need to set on first node out of IF block
  if (previousNode && previousNode.parallelKey && foundParallelNodesMap[previousNode.parallelKey] && ((!currentNode.parallelKey) || (nodeType && (nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLEL)))) {
      // first node outside of PARALLEL block
      parallelNode = foundParallelNodesMap[previousNode.parallelKey];

      previousNodeType = FlowDiagramBuilderApi.getInstanceRecordHashNodeType(previousNode.key);
      // if previousNode was a part of a decision block and currentNode type is DECISIONBLOCK (which means previousNode
      // was involved in a decisionBlock and currentNode is beginning of a new decisionBlock under a same make-a-decision
      // flow logic) or PARALLELBLOCK, current parallel logic does not end with the currentNode. Rather, current parallel logic ends
      // when current make-a-decision flow logic ends.
      if ((previousNodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK || previousNode.decisionBlockKey) &&
          nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK || nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK) {
          parallelNodeActiveByDecision = parallelNode;
      } else if (!FlowDiagramBuilderApi.isElseBlock(previousNode, currentNode, sourceIfNode, nodeType)) {
          FlowDiagramBuilderApi.pushEndOfParallelKeys(currentNode, parallelNode.key);
          if (currentParallelNode && currentParallelNode.key === parallelNode.key) {
              currentParallelNode = null;
          }
          // remove from foundParallelNodes
          for (k = 0; k < foundParallelNodes.length; k++) {
              FlowDiagramBuilderApi.removeFromArrayByKey(foundParallelNodes, "key", parallelNode.key);
          }
      } else {
          // add endOfIfKey to foundParallelNodes
          for (k = 0; k < foundParallelNodes.length; k++) {
              if (foundParallelNodes[k].key === parallelNode.key) {
                  foundParallelNodes[k].endOfIfKey = sourceIfNode.key;
                  if (currentParallelNode && currentParallelNode.key === parallelNode.key) {
                      currentParallelNode = null;
                  }
                  break;
              }
          }
      }
  }

  if (foundParallelNodes.length > 0) {
      // a PARALLEL node can also be end of PARALLEL from a previous PARALLEL node but its children cannot
      if (!currentNode.parallelKey || (nodeType && nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLEL)) {
          // loop foundParallelNodes check if we should set endOfParallelKeys on currentNode
          var copyOfFoundParallelNodes = JSON.parse(JSON.stringify(foundParallelNodes));
          for (j = 0; j < copyOfFoundParallelNodes.length; j++) {
              parallelNode = copyOfFoundParallelNodes[j];
              if (parallelNode.endOfIfKey) {
                  if (currentNode.endOfIfKeys && parallelNode.endOfIfKey === currentNode.endOfIfKeys) {
                      FlowDiagramBuilderApi.pushEndOfParallelKeys(currentNode, parallelNode.key);
                      if (currentParallelNode && currentParallelNode.key === parallelNode.key) {
                          currentParallelNode = null;
                      }
                      FlowDiagramBuilderApi.removeFromArrayByKey(foundParallelNodes, "key", parallelNode.key);
                  }
              }
          }
      }
  }
  return {
      parallelNodeActiveByDecision: parallelNodeActiveByDecision,
      currentParallelNode: currentParallelNode
  };
};

FlowDiagramBuilderApi.processDecision = function(nodeType, currentNode, previousNode, sourceIfNode, decisionNodeActiveByParallel, currentDecisionNode, foundDecisionNodes, foundDecisionNodesMap) {
  // 0. if decisionNodeActiveByParallel then there is an active decision logic that has not ended because of
  // make-a-parallel flow logic. If the current node is not part a PARALLEL BLOCK logic, then claim the
  // end of the active decision logic. Another way for the active decision logic to end with
  // decisionNodeActiveByParallel (having a value) is reaching the end node, which will claim
  // end of all active decision logic.
  var decisionNode;
  var previousNodeType;
  if (decisionNodeActiveByParallel && !(nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK || currentNode.parallelBlockKey)) {
      decisionNode = decisionNodeActiveByParallel;
      decisionNodeActiveByParallel = null;
      FlowDiagramBuilderApi.pushEndOfDecisionKeys(currentNode, decisionNode.key);
      if (currentDecisionNode && currentDecisionNode.key === decisionNode.key) {
          currentDecisionNode = null;
      }
      // remove from foundParallelNodes
      for (k = 0; k < foundDecisionNodes.length; k++) {
          FlowDiagramBuilderApi.removeFromArrayByKey(foundDecisionNodes, "key", decisionNode.key);
      }
  }

  // 1. set the decisionKey attribute
  if (nodeType && (nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISION)) {
      currentDecisionNode = currentNode;
      foundDecisionNodesMap[currentNode.key] = currentNode;
      foundDecisionNodes.push({
          key: currentNode.key
      });
      currentNode.decisionKey = currentNode.key;
      FlowDiagramBuilderApi.updateInstanceRecordHash(currentNode.key, "decisionKey", currentNode.key);
  } else if (currentDecisionNode && (currentNode.depth > currentDecisionNode.depth || currentNode.decisionBlockKey || (nodeType && nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK))) {
      // is a child of DECISION node
      currentNode.decisionKey = currentDecisionNode.key;
      FlowDiagramBuilderApi.updateInstanceRecordHash(currentNode.key, "decisionKey", currentDecisionNode.key);
  }

  // 2. set the endOfDecisionKeys attribute on first node outside of DECISION block, or set flag endOfIfKey if we need to set on first node out of IF block
  if (previousNode && previousNode.decisionKey && foundDecisionNodesMap[previousNode.decisionKey] && ((!currentNode.decisionKey) || (nodeType && (nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISION)))) {
      // first node outside of DECISION block
      decisionNode = foundDecisionNodesMap[previousNode.decisionKey];

      previousNodeType = FlowDiagramBuilderApi.getInstanceRecordHashNodeType(previousNode.key);
      // if previousNode was a part of a decision block and currentNode type is DECISIONBLOCK (which means previousNode
      // was involved in a decisionBlock and currentNode is beginning of a new decisionBlock under a same make-a-decision
      // flow logic) or PARALLELBLOCK, current parallel logic does not end with the currentNode. Rather, current decision logic ends
      // when current make-a-decision flow logic ends.
      if ((previousNodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK || previousNode.decisionBlockKey) &&
          nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK || nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK) {
          //nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK ) {
          decisionNodeActiveByParallel = decisionNode;
      } else if (!FlowDiagramBuilderApi.isElseBlock(previousNode, currentNode, sourceIfNode, nodeType)) {
          FlowDiagramBuilderApi.pushEndOfDecisionKeys(currentNode, decisionNode.key);
          if (currentDecisionNode && currentDecisionNode.key === decisionNode.key) {
              currentDecisionNode = null;
          }
          // remove from foundDecisionNodes
          for (k = 0; k < foundDecisionNodes.length; k++) {
              FlowDiagramBuilderApi.removeFromArrayByKey(foundDecisionNodes, "key", decisionNode.key);
          }
      } else {
          // add endOfIfKey to foundParallelNodes
          for (k = 0; k < foundDecisionNodes.length; k++) {
              if (foundDecisionNodes[k].key === decisionNode.key) {
                  foundDecisionNodes[k].endOfIfKey = sourceIfNode.key;
                  if (currentDecisionNode && currentDecisionNode.key === decisionNode.key) {
                      currentDecisionNode = null;
                  }
                  break;
              }
          }
      }
  }

  if (foundDecisionNodes.length > 0) {
      // a DECISION node can also be end of DECISION from a previous DECISION node but its children cannot
      if (!currentNode.decisionKey || (nodeType && nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISION)) {
          // loop foundDecisionNodes check if we should set endOfDecisionKeys on currentNode
          var copyOfFoundDecisionNodes = JSON.parse(JSON.stringify(foundDecisionNodes));
          for (j = 0; j < copyOfFoundDecisionNodes.length; j++) {
              decisionNode = copyOfFoundDecisionNodes[j];
              if (decisionNode.endOfIfKey) {
                  if (currentNode.endOfIfKeys && decisionNode.endOfIfKey === currentNode.endOfIfKeys) {
                      FlowDiagramBuilderApi.pushEndOfDecisionKeys(currentNode, decisionNode.key);
                      if (currentDecisionNode && currentDecisionNode.key === decisionNode.key) {
                          currentDecisionNode = null;
                      }
                      FlowDiagramBuilderApi.removeFromArrayByKey(foundDecisionNodes, "key", decisionNode.key);
                  }
              }
          }
      }
  }
  return {
      decisionNodeActiveByParallel: decisionNodeActiveByParallel,
      currentDecisionNode: currentDecisionNode
  };
};

FlowDiagramBuilderApi.getConfiguration = function() {
  return FlowDiagramDatabaseApi.getConfiguration(FlowDiagramConstants.SYS_IDS.SN_FLOW_DIAGRAM_BUILDER_CONFIGURATION);
};

// put the nodes back to the ascending order because the matched nodes from the flow-diagramming search need to be in the ascending order.
FlowDiagramBuilderApi.assignSortedNodesToDataModel = function(dataModel) {
  var nodes = dataModel.diagramJSON.nodes;
  dataModel.diagramJSON.nodes = FlowDiagramBuilderApi.sortNodes(nodes);
};

FlowDiagramBuilderApi.isValidArrayPosition = function(arrayPosition) {
  if ((arrayPosition === 0 || arrayPosition === "0") || arrayPosition) {
      return true;
  } else {
      return false;
  }
};

// order is optional and input can be "asc" and "desc". "asc" if order is not present
FlowDiagramBuilderApi.sortNodes = function(nodes, order) {
  var node;
  for (i = 0; i < nodes.length; i++) {
      node = nodes[i];
      if (node.key === FlowDiagramBuilderApi.endKey) {
          node.arrayPosition = FlowDiagramBuilderApi.maxInteger;
      } else if (!(FlowDiagramBuilderApi.isValidArrayPosition(node.arrayPosition))) {
          node.arrayPosition = -1;
      }
  }
  var orderedNodes;
  if (order) {
      orderedNodes = FlowDiagramHelper.sortByArrayPosition(nodes, order);
  } else {
      orderedNodes = FlowDiagramHelper.sortByArrayPosition(nodes);
  }
  for (i = 0; i < orderedNodes.length; i++) {
      node = orderedNodes[i];
      if (node.key === FlowDiagramBuilderApi.endKey || node.arrayPosition === -1) {
          delete node.arrayPosition;
      }
  }
  return orderedNodes;
};

// 1) for every node that has loopback edge, record the loopback edge's destination as loopBackNodes in FlowDiagramBuilderApi.instanceRecordsHash
// 2) find all edges and store loopback edges and non-loopback edges separately based on edge source node
// then for every loopback edge, get a list of non-loopback edges if source node of loopback edge equals source node of non-loopback edge
// create loopExitNodeHash and for every designation(to) node of non-loopback edge, identify designation (to) node of the loopback edge that
// share the same source node
FlowDiagramBuilderApi.setupLoopExitNodeHashAndloopBackNodes = function(nodes, edges) {
  var i, j;
  var loopExitNodeHash = {};
  var loopbackEdgeHash = {};
  var nonLoopbackEdgeHash = {};
  for (i = 0; i < edges.length; i++) {
      var currentEdgeFrom = edges[i].from;
      var currentEdgeTo = edges[i].to;
      if (edges[i].toPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID) {
          if (!loopbackEdgeHash[currentEdgeFrom])
              loopbackEdgeHash[currentEdgeFrom] = [];
          if (FlowDiagramBuilderApi.instanceRecordsHash[currentEdgeFrom] &&
              FlowDiagramBuilderApi.instanceRecordsHash[currentEdgeTo]) {
              if (!FlowDiagramBuilderApi.instanceRecordsHash[currentEdgeFrom].loopBackNodes)
                  FlowDiagramBuilderApi.instanceRecordsHash[currentEdgeFrom].loopBackNodes = [];

              // Remember that loopback can point to itself and therefore there could be a record in
              // FlowDiagramBuilderApi.instanceRecordsHash[currentEdgeFrom].loopBackNodes that points to
              // FlowDiagramBuilderApi.instanceRecordsHash[currentEdgeFrom]
              FlowDiagramBuilderApi.instanceRecordsHash[currentEdgeFrom].loopBackNodes.push(FlowDiagramBuilderApi.instanceRecordsHash[currentEdgeTo]);
          }
          loopbackEdgeHash[currentEdgeFrom].push(currentEdgeTo);
      } else {
          if (!nonLoopbackEdgeHash[currentEdgeFrom])
              nonLoopbackEdgeHash[currentEdgeFrom] = [];
          nonLoopbackEdgeHash[currentEdgeFrom].push(currentEdgeTo);
      }
  }
  for (var key in loopbackEdgeHash) {
      if (Object.prototype.hasOwnProperty.call(loopbackEdgeHash, key)) {
          var loopbackEdges = loopbackEdgeHash[key];
          for (i = 0; i < loopbackEdges.length; i++) {
              var loopStartNodeKey = loopbackEdges[i];
              var loopExitEdges = nonLoopbackEdgeHash[key];
              if (loopExitEdges) {
                  for (j = 0; j < loopExitEdges.length; j++) {
                      var loopExitEdgeTo = loopExitEdges[j];
                      if (!loopExitNodeHash[loopExitEdgeTo])
                          loopExitNodeHash[loopExitEdgeTo] = {};

                      if (!loopExitNodeHash[loopExitEdgeTo][loopStartNodeKey])
                          loopExitNodeHash[loopExitEdgeTo][loopStartNodeKey] = loopStartNodeKey;
                  }
              }
          }
      }
  }
  return loopExitNodeHash;
};

// for the target "node", returns all end of if nodes and starting for loop nodes
FlowDiagramBuilderApi.getSourceNodes = function(node, loopExitNodeHash) {
  var i;
  var sourceNodes = [];
  if (node.endOfIfKeys) {
      var endOfIfkeys = node.endOfIfKeys.split(',');
      for (i = 0; i < endOfIfkeys.length; i++) {
          var sourceIfKey = endOfIfkeys[i];
          var sourceIfNode = FlowDiagramBuilderApi.instanceRecordsHash[sourceIfKey];
          if (sourceIfNode)
              sourceNodes.push(sourceIfNode);
      }
  }
  var loopExitNodes = loopExitNodeHash[node.key];
  if (loopExitNodes) {
      var processedLoopStartKeyHash = {};
      for (var key in loopExitNodes) {
          if (Object.prototype.hasOwnProperty.call(loopExitNodes, key)) {
              var loopStartKey = loopExitNodes[key];
              if (!processedLoopStartKeyHash[loopStartKey]) {
                  processedLoopStartKeyHash[loopStartKey] = true;
                  if (FlowDiagramBuilderApi.instanceRecordsHash[loopStartKey])
                      sourceNodes.push(FlowDiagramBuilderApi.instanceRecordsHash[loopStartKey]);
              }
          }
      }
  }
  if (node.endOfParallelKeys) {
      for (i = 0; i < node.endOfParallelKeys.length; i++) {
          sourceNodes.push(FlowDiagramBuilderApi.instanceRecordsHash[node.endOfParallelKeys[i]]);
      }
  }
  if (node.endOfDecisionKeys) {
      for (i = 0; i < node.endOfDecisionKeys.length; i++) {
          sourceNodes.push(FlowDiagramBuilderApi.instanceRecordsHash[node.endOfDecisionKeys[i]]);
      }
  }
  return sourceNodes;
};

// assuming that the newNodeList is sorted, the function will iterate through
// newNodeList in order and return node that has depth lower than newNode
// toNode is valid only if it is part of newNodesPerNodeHash; newNodesPerNodeHash contains all end nodes created for "node"
FlowDiagramBuilderApi.getToNode = function(newNodeList, newNode, newNodesPerNodeHash) {
  var node = null;
  var sourceIfKeys = null;
  if (newNode.sourceIfNodeKey) {
      sourceIfKeys = FlowDiagramBuilderApi.getSourceIfKeysFromNodeKey(newNode.sourceIfNodeKey);
      if (sourceIfKeys && sourceIfKeys instanceof Array && sourceIfKeys.length > 0) {
          sourceIfKeys = sourceIfKeys.join(',');
      }
  }

  for (var i = 0; i < newNodeList.length; i++) {
      var sourceIfNodeKeyMatch = true;

      // check to see if newNode's sourceIfNodeKey's sourceIfKeys include tempNode's sourceIfNodeKey if newNode is endif and tempNode is endif
      // which means tempNode's sourceIfNodeKey is an ancestor of newNode's sourceIfNodeKey
      if (newNodeList[i].depth < newNode.depth) {
          var tempNode = newNodeList[i];

          if (sourceIfKeys && tempNode.sourceIfNodeKey) {
              sourceIfNodeKeyMatch = false;
              if (sourceIfKeys.includes(tempNode.sourceIfNodeKey))
                  sourceIfNodeKeyMatch = true;
          }

          if (newNodesPerNodeHash[tempNode.key] && sourceIfNodeKeyMatch) {
              var result = FlowDiagramBuilderApi.processNodeIfTempNodeAndNewNodeIsUnderParallelOrDecision(tempNode, newNode);
              if (result) {
                  node = result;
                  break;
              }
          }
      }
  }
  return node;
};

FlowDiagramBuilderApi.processNodeIfTempNodeAndNewNodeIsUnderParallelOrDecision = function(tempNode, newNode) {
  var node;
  // if tempNode is ENDPARALLEL node and the newNode (END OF node) is under a parallel node (but not Decision type), then
  // return the tempNode only if it is the parallelNode that the newNode is under (newNode.parallelKey)
  // if tempNode is ENDDECISION node and the newNode (END OF node) is under a decision node (but not Parallel type), then
  // return the tempNode only if it is the decisionNode that the newNode is under (newNode.decisionKey)

  if (tempNode && newNode) {
      var paralleKeyMatchIfEndParallelAndNewNodeUnderParallelOrDecisionKey = true;

      if (tempNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDPARALLEL && newNode.parallelKey && !newNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDECISION) {
          paralleKeyMatchIfEndParallelAndNewNodeUnderParallelOrDecisionKey = false;
          if (tempNode.parallelKey === newNode.parallelKey) {
              paralleKeyMatchIfEndParallelAndNewNodeUnderParallelOrDecisionKey = true;
          }
      } else if (tempNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDECISION && newNode.decisionKey && !newNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDPARALLEL) {
          paralleKeyMatchIfEndParallelAndNewNodeUnderParallelOrDecisionKey = false;
          if (tempNode.decisionKey === newNode.decisionKey) {
              paralleKeyMatchIfEndParallelAndNewNodeUnderParallelOrDecisionKey = true;
          }
      }

      if (paralleKeyMatchIfEndParallelAndNewNodeUnderParallelOrDecisionKey) {
          node = tempNode;
      }
  }
  return node;
};

FlowDiagramBuilderApi.getToEndNodeToPort = function(type) {
  var toPort = null;
  if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP)
      toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_LOOP_INPUT_SYS_ID;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP)
      toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_DO_LOOP_INPUT_SYS_ID;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDIF)
      toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_IF_INPUT_SYS_ID;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDPARALLEL)
      toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_PARALLEL_INPUT_SYS_ID;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDECISION)
      toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_DECISION_INPUT_SYS_ID;
  return toPort;
};

FlowDiagramBuilderApi.removeAllLoopbackEdges = function(edges) {
  // remove all loopback edges since these will be added back by ENDLOOP node implementation
  // the loopback edge data has been stored by setupLoopExitNodeHashAndloopBackNodes()
  for (var i = 0; i < edges.length; i++) {
      if (edges[i].toPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID) {
          edges.splice(i, 1);
          i--;
      }
  }
};

FlowDiagramBuilderApi.findNodesWithAtLeastOneBranch = function(nodes) {
  // map of 1-to-many nodes with at least one parallel branch or one decision branch
  var branchMap = {};
  for (var k = 0; k < nodes.length; k++) {
      node = nodes[k];
      if (node.name === FlowDiagramConstants.NODE_NAMES.PARALLEL_BRANCH || node.node_id === FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_DECISION_BLOCK_ACTION) {
          branchMap[node.parallelKey] = true;
      }
  }
  return branchMap;
};

FlowDiagramBuilderApi.isTypeEndNode = function(instanceType) {
  if (
      instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDIF ||
      instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP ||
      instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP ||
      instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDPARALLEL ||
      instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDECISION
  ) {
      return true;
  }
  return false;
};

FlowDiagramBuilderApi.addLoopbackEdge = function(edges, newNode, sourceNode) {
  // if the newly created END node is an endloop node, then add a loopback edge to the orginal
  // FOR LOOP node
  if (newNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP) {
      var newEdgeLoopBack = FlowDiagramTranslateModel.getEdgeModel();
      if (newEdgeLoopBack) {
          newEdgeLoopBack = FlowDiagramBuilderApi.updateEdge(newEdgeLoopBack, {
              from: newNode.key,
              to: sourceNode.key,
              fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_LOOP_OUTPUT_SYS_ID,
              toPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID,
              edgeId: FlowDiagramBuilderApi.edgeId,
              linkLabel: FlowDiagramConstants.LINK_LABELS.loopBackConnector,
              labelColor: FlowDiagramConstants.LINK_LABEL_COLORS.loop,
              addBtnVisibility: FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopLoopback,
          });
      }
      edges.push(newEdgeLoopBack);
  } else if (newNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP) {
      var newEdgeLoopBack = FlowDiagramTranslateModel.getEdgeModel();
      if (newEdgeLoopBack) {
          newEdgeLoopBack = FlowDiagramBuilderApi.updateEdge(newEdgeLoopBack, {
              from: newNode.key,
              to: sourceNode.key,
              fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_DO_LOOP_OUTPUT_LOOPBACK_SYS_ID,
              toPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID,
              edgeId: FlowDiagramBuilderApi.edgeId,
              linkLabel: FlowDiagramConstants.LINK_LABELS.loopBackConnector,
              labelColor: FlowDiagramConstants.LINK_LABEL_COLORS.loop,
              addBtnVisibility: FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopLoopback,
          });
      }
      edges.push(newEdgeLoopBack);
  }
};

FlowDiagramBuilderApi.processEndNodes = function(nodes, edges, loopExitNodeHash) {
  var i, j, k;
  var node;
  var newNodesBySourceKeyHash = {};
  var newNodeList = [];

  // remove all loopback edges since these will be added back by ENDLOOP node implementation
  FlowDiagramBuilderApi.removeAllLoopbackEdges(edges);

  // map of 1-to-many nodes with more than one parallel or decision branch
  var hasBranchMap = FlowDiagramBuilderApi.findNodesWithAtLeastOneBranch(nodes);

  nodes = FlowDiagramBuilderApi.sortNodes(nodes, "desc");
  for (i = 0; i < nodes.length; i++) {
      node = nodes[i];
      var newNodesPerNodeHash = {};
      var isEndNode = FlowDiagramBuilderApi.isTypeEndNode(node.instanceType);
      if (!isEndNode) {
          // for the target "node", returns all end of if nodes and starting for loop nodes
          var sourceNodes = FlowDiagramBuilderApi.getSourceNodes(node, loopExitNodeHash);

          // sourceNodes exists for the node.key which means that there is either a loopback edge from this node
          // or if statement ends with this node
          if (sourceNodes.length > 0) {
              if (FlowDiagramHelper.hasProperty(sourceNodes[0], "customOrder")) {
                  sourceNodes = FlowDiagramHelper.sortByCustomOrder(sourceNodes);
              } else {
                  sourceNodes = FlowDiagramHelper.sortByDepth(sourceNodes);
              }
              var lastCreatedNode = null;
              var lastCreatedNodeFromPort;
              var lastCreatedNodeType;
              var newEdge;
              var newEdgeLabel = "";
              var anyEndNodeAdded = false;
              var toNode;
              var toNodeToPort;

              // reviewing sourceNode that has lowest depth first, which should be connected to node
              for (j = 0; j < sourceNodes.length; j++) {
                  var sourceNode = sourceNodes[j];

                  // if new END NODE has not been created for the target sourceNode.key
                  if (!newNodesBySourceKeyHash[sourceNode.key]) {
                      anyEndNodeAdded = true;
                      var newNodeType;
                      var newNodeFromPort;
                      if (sourceNode.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF) {
                          newNodeType = FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDIF;
                          newNodeFromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_IF_OUTPUT_SYS_ID;
                      } else if (sourceNode.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.FOREACH) {
                          newNodeType = FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP;
                          newNodeFromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_LOOP_OUTPUT_SYS_ID;
                      } else if (sourceNode.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DOUNTIL) {
                          newNodeType = FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP;
                          newNodeFromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_DO_LOOP_OUTPUT_SYS_ID;
                      } else if (sourceNode.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLEL) {
                          newNodeType = FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDPARALLEL;
                          newNodeFromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_PARALLEL_OUTPUT_SYS_ID;
                      } else if (sourceNode.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISION) {
                          newNodeType = FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDECISION;
                          newNodeFromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_DECISION_OUTPUT_SYS_ID;
                      }
                      // create END node for each unique sourceNode
                      // First END node should have highest depth among END nodes
                      var newNode = FlowDiagramBuilderApi.createHiddenNode(nodes, sourceNode.key, sourceNode.parallelKey, sourceNode.decisionKey, newNodeType, sourceNode.depth, false);
                      if (sourceNode.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DOUNTIL) {
                          newNode.name = FlowDiagramHelper.generateNameWithCondition(FlowDiagramConstants.NODE_NAMES.UNTIL, sourceNode.data);
                          newNode.order = sourceNode.orderBottom;
                          newNode.arrayPosition = sourceNode.arrayPosition;
                          newNode.icon = sourceNode.icon;
                      }
                      if (FlowDiagramHelper.hasProperty(sourceNode, "customOrder")) {
                          newNode.customOrder = sourceNode.customOrder;
                      }

                      // if it is not the first END node created for the node, create an edge between last END node
                      // created and current END node created
                      if (j !== 0) {

                          // toNode cannot be node on same/lower depth and thus newNode should be connected to closest node that is not on the same/lower depth
                          // toNode is valid only if it is part of newNodesPerNodeHash; newNodesPerNodeHash contains all end nodes created for "node"
                          toNode = FlowDiagramBuilderApi.getToNode(newNodeList, newNode, newNodesPerNodeHash);
                          if (toNode) {
                              if (toNode.instanceType) {
                                  toNodeToPort = FlowDiagramBuilderApi.getToEndNodeToPort(toNode.instanceType);
                              }

                              newEdge = FlowDiagramTranslateModel.getEdgeModel();
                              if (newEdge) {
                                  newEdge = FlowDiagramBuilderApi.updateEdge(
                                      newEdge, {
                                          from: newNode.key,
                                          to: toNode.key,
                                          fromPort: newNodeFromPort,
                                          toPort: toNodeToPort,
                                          edgeId: FlowDiagramBuilderApi.edgeId,
                                      }
                                  );

                                  if (newNodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP || newNodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP) {
                                      newEdge.linkLabel = FlowDiagramConstants.LINK_LABELS.loopContinueConnector;
                                      newEdge.labelColor = FlowDiagramConstants.LINK_LABEL_COLORS.loop;
                                  }

                                  edges.push(newEdge);
                              }
                          }
                      }
                      newNodesBySourceKeyHash[sourceNode.key] = newNode;
                      newNodesPerNodeHash[newNode.key] = newNode;
                      newNodeList.push(newNode);
                      if (FlowDiagramHelper.hasProperty(sourceNode, "customOrder")) {
                          newNodeList = FlowDiagramHelper.sortByCustomOrder(newNodeList, 'desc');
                      } else {
                          newNodeList = FlowDiagramHelper.sortByDepth(newNodeList, 'desc');
                      }

                      // if the newly created END node is an endloop node, then add a loopback edge to the orginal
                      // FOR LOOP node
                      FlowDiagramBuilderApi.addLoopbackEdge(edges, newNode, sourceNode);

                      if (lastCreatedNode === null) {
                          lastCreatedNode = newNode;
                          lastCreatedNodeType = newNodeType;
                          if (newNodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP)
                              lastCreatedNodeFromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_LOOP_OUTPUT_SYS_ID;
                          else if (newNodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP)
                              lastCreatedNodeFromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_DO_LOOP_OUTPUT_SYS_ID;
                          else if (newNodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDIF)
                              lastCreatedNodeFromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_IF_OUTPUT_SYS_ID;
                          else if (newNodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDPARALLEL)
                              lastCreatedNodeFromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_PARALLEL_OUTPUT_SYS_ID;
                          else if (newNodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDECISION)
                              lastCreatedNodeFromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_DECISION_OUTPUT_SYS_ID;
                      }
                  }
              }

              // true if any end node have been added for the target "node"
              if (anyEndNodeAdded) {
                  var toPort;
                  // all edges pointing to target "node" should point to newly created END nodes
                  // if there are multiples of END nodes then decide by highest depth value
                  for (k = 0; k < edges.length; k++) {
                      var currentEdgeFrom = edges[k].from;
                      var currentEdgeTo = edges[k].to;
                      var currentEdgeToPort = edges[k].toPort;
                      if (currentEdgeTo === node.key &&
                          currentEdgeToPort !== FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID) {

                          // highestDepthLoopBackNode points to FOR LOOP node that ends with target "node" that has the
                          // highest depth value, which means that the FOR LOOP node is closest to the target "node" among
                          // other FOR LOOP nodes that ends with target "node"
                          var highestDepthLoopBackNode = null;
                          if (FlowDiagramBuilderApi.instanceRecordsHash[currentEdgeFrom] &&
                              FlowDiagramBuilderApi.instanceRecordsHash[currentEdgeFrom].loopBackNodes) {
                              var loopBackNodes = FlowDiagramBuilderApi.instanceRecordsHash[currentEdgeFrom].loopBackNodes;
                              if (loopBackNodes && loopBackNodes instanceof Array && loopBackNodes.length > 0) {
                                  loopBackNodes = FlowDiagramHelper.sortByDepth(loopBackNodes, 'desc');
                                  highestDepthLoopBackNode = loopBackNodes[0];
                              }
                          }
                          // sourceKey is the source if node associated with currentEdgeFrom
                          var sourceKey = null;
                          var parallelKey = null;
                          var decisionKey = null;
                          var currentEdgeFromNode = FlowDiagramBuilderApi.instanceRecordsHash[currentEdgeFrom];
                          if (currentEdgeFromNode) {
                              sourceKey = FlowDiagramBuilderApi.getImmediateSourceIfKeyFromNodeKey(currentEdgeFrom);
                              if (currentEdgeFromNode.parallelKey) {
                                  parallelKey = currentEdgeFromNode.parallelKey;
                              }
                              if (currentEdgeFromNode.decisionKey) {
                                  decisionKey = currentEdgeFromNode.decisionKey;
                              }
                          }
                          toNode = FlowDiagramBuilderApi.getCurrentEdgeFromToNode(newNodesBySourceKeyHash, sourceKey, parallelKey, decisionKey, highestDepthLoopBackNode);

                          // if Do Parallel has branches then remove the edge from ENDPARALLEL to Do Parallel
                          if (toNode) {
                              if (toNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP)
                                  toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_LOOP_INPUT_SYS_ID;
                              else if (toNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP)
                                  toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_DO_LOOP_INPUT_SYS_ID;
                              else if (toNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDIF)
                                  toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_IF_INPUT_SYS_ID;
                              else if (toNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDPARALLEL)
                                  toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_PARALLEL_INPUT_SYS_ID;
                              else if (toNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDECISION)
                                  toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_DECISION_INPUT_SYS_ID;
                              edges[k].to = toNode.key;
                              edges[k].toPort = toPort;

                              var removeParallelLoopbackEdge = toNode.endParallelNodeKey && toNode.endParallelNodeKey === edges[k].from && hasBranchMap[toNode.endParallelNodeKey];
                              var removeDecisionLoopbackEdge = toNode.endDecisionNodeKey && toNode.endDecisionNodeKey === edges[k].from && hasBranchMap[toNode.endDecisionNodeKey];

                              if (removeParallelLoopbackEdge || removeDecisionLoopbackEdge) {
                                  edges.splice(k, 1);
                              }
                          }
                      }
                  }

                  // If the current node type is not FLOW_LOGIC_PATH_BROKEN_ACTION then an edge must be created
                  // that connects between the node and the lastly created END IF or END LOOP node
                  // Please note that all END nodes added will be placed/ordered before target "node"
                  var nodeFromInstanceRecordHash = FlowDiagramBuilderApi.instanceRecordsHash[node.key];
                  if (node.key === FlowDiagramBuilderApi.endKey ||
                      (nodeFromInstanceRecordHash && nodeFromInstanceRecordHash.type !== FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_PATH_BROKEN_ACTION)) {
                      // endKey check must be first
                      if (node.key === FlowDiagramBuilderApi.endKey) {
                          toPort = FlowDiagramBuilderApi.endPort;
                      } else {
                          toPort = FlowDiagramBuilderApi.getToPort(nodeFromInstanceRecordHash.type);
                      }

                      newEdge = FlowDiagramTranslateModel.getEdgeModel();
                      if (newEdge) {
                          newEdge = FlowDiagramBuilderApi.updateEdge(
                              newEdge, {
                                  from: lastCreatedNode.key,
                                  to: node.key,
                                  fromPort: lastCreatedNodeFromPort,
                                  toPort: toPort,
                                  edgeId: FlowDiagramBuilderApi.edgeId,
                                  label: newEdgeLabel,
                              }
                          );

                          if (lastCreatedNodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP ||
                              lastCreatedNodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP) {
                              newEdge.linkLabel = FlowDiagramConstants.LINK_LABELS.loopContinueConnector;
                              newEdge.labelColor = FlowDiagramConstants.LINK_LABEL_COLORS.loop;
                          }

                          edges.push(newEdge);
                      }
                  }
              }
          }
      }
  }
  return nodes;
};

FlowDiagramBuilderApi.getToPort = function(type) {
  var toPort;
  if (type === FlowDiagramConstants.INSTANCE_TYPES.ACTION_INSTANCE_TYPE) {
      toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ACTION_INPUT_SYS_ID;
  } else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF ||
      type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE ||
      type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF) {
      toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_INPUT_SYS_ID;
  } else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.FOREACH || type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DOUNTIL) {
      toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_NORMAL_SYS_ID;
  } else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.END) {
      toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_INPUT_SYS_ID;
  } else if (Object.keys(FlowDiagramConstants.FLOW_LOGIC_TYPES.ONE_TO_ONE).toString().includes(type)) {
      toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_ONE_INPUT;
  } else if (Object.keys(FlowDiagramConstants.FLOW_LOGIC_TYPES.ONE_TO_MANY).toString().includes(type)) {
      toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_MANY_INPUT;
  } else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK) {
      toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_PARALLELBLOCK_INPUT_SYS_ID;
  } else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK) {
      toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_DECISIONBLOCK_INPUT_SYS_ID;
  }
  return toPort;
};

// return the node with highest depth by comparing the source IF, FOR, PARALLEL, DECISION nodes if the nodes are present
FlowDiagramBuilderApi.getCurrentEdgeFromToNode = function(newNodesBySourceKeyHash, sourceKey, parallelKey, decisionKey, highestDepthLoopBackNode) {
  var sourceKeyNode = FlowDiagramBuilderApi.instanceRecordsHash[sourceKey];
  var parallelKeyNode = FlowDiagramBuilderApi.instanceRecordsHash[parallelKey];
  var decisionKeyNode = FlowDiagramBuilderApi.instanceRecordsHash[decisionKey];
  var toNode = null;
  var ifDepth = -1;
  var parallelDepth = -1;
  var decisionDepth = -1;
  var forDepth = -1;
  if (sourceKeyNode && sourceKeyNode.depth) {
      ifDepth = sourceKeyNode.depth;
  }
  if (parallelKeyNode && parallelKeyNode.depth) {
      parallelDepth = parallelKeyNode.depth;
  }
  if (decisionKeyNode && decisionKeyNode.depth) {
      decisionDepth = decisionKeyNode.depth;
  }
  if (highestDepthLoopBackNode && highestDepthLoopBackNode.depth) {
      forDepth = highestDepthLoopBackNode.depth;
  }
  if (sourceKeyNode && ifDepth > parallelDepth && ifDepth > decisionDepth && ifDepth > forDepth) {
      toNode = newNodesBySourceKeyHash[sourceKey];
  } else if (parallelKeyNode && parallelDepth > ifDepth && parallelDepth > forDepth && parallelDepth > decisionDepth) {
      toNode = newNodesBySourceKeyHash[parallelKey];
  } else if (decisionKeyNode && decisionDepth > ifDepth && decisionDepth > forDepth && decisionDepth > parallelDepth) {
      toNode = newNodesBySourceKeyHash[decisionKey];
  } else if (highestDepthLoopBackNode && forDepth > ifDepth && forDepth > parallelDepth && forDepth > decisionDepth) {
      toNode = newNodesBySourceKeyHash[highestDepthLoopBackNode.key];
  }
  return toNode;
};

FlowDiagramBuilderApi.decorateLinks = function(dataModel, nodeMap) {
  function isTrueConnector(edge, fromNode) {
      if (fromNode) {
          return fromNode.node_id === FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_CONDITIONAL_DIAGRAM_ACTION && edge.fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_TRUE_SYS_ID;
      }
      return false;
  }

  function isFalseConnector(edge, fromNode) {
      if (fromNode) {
          return fromNode.node_id === FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_CONDITIONAL_DIAGRAM_ACTION && edge.fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_FALSE_SYS_ID;
      }
      return false;
  }

  function isLoopContinueConnector(edge, fromNode) {
      if (fromNode) {

          if (fromNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP) {
              return fromNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP && edge.toPort !== FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID;
          } else if (fromNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP) {
              return fromNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP && edge.toPort !== FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_DO_LOOP_BACK_SYS_ID;
          }
      }
      return false;
  }

  function isLoopBackConnector(edge, fromNode) {
      if (fromNode) {
          if (fromNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP) {
              return fromNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP && edge.toPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID;
          } else if (fromNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP) {
              return fromNode.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP && edge.toPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_DO_LOOP_BACK_SYS_ID;
          }
      }
      return false
  }

  function isDecisionConnector(fromNode) {
      if (fromNode && fromNode.data && FlowDiagramHelper.hasProperty(fromNode.data, "decision_label")) {
          return true;
      }
      return false;
  }

  function isFromPathBrokenConnector(edge, fromNode) {
      if (fromNode) {
          return fromNode.node_id === FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_PATH_BROKEN_ACTION && edge.toPort !== FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID;
      }
      return false;
  }

  function isGoBackToConnector(edge) {
      return edge.fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_GOBACKTO_OUTPUT_SYS_ID;
  }

  dataModel.diagramJSON.edges.forEach(function(edge) {
      var fromNode = nodeMap[edge.from];
      var toNode = nodeMap[edge.to];
      if (isTrueConnector(edge, fromNode)) {
          edge.labelSegmentIndex = FlowDiagramConstants.LINK_LABEL_SEGMENT.trueConnector.segmentIndex;
          edge.labelSegmentFraction = FlowDiagramConstants.LINK_LABEL_SEGMENT.trueConnector.segmentFraction;
          edge.labelAlignmentFocus = FlowDiagramConstants.LINK_LABEL_SEGMENT.trueConnector.alignmentFocus;
          edge.addBtnSegmentIndex = FlowDiagramConstants.LINK_ADD_BTN_SEGMENT.trueConnector.segmentIndex;
          edge.addBtnSegmentFraction = FlowDiagramConstants.LINK_ADD_BTN_SEGMENT.trueConnector.segmentFraction;
      } else if (isFalseConnector(edge, fromNode)) {
          edge.labelSegmentIndex = FlowDiagramConstants.LINK_LABEL_SEGMENT.falseConnector.segmentIndex;
          edge.labelSegmentFraction = FlowDiagramConstants.LINK_LABEL_SEGMENT.falseConnector.segmentFraction;
          edge.labelAlignmentFocus = FlowDiagramConstants.LINK_LABEL_SEGMENT.falseConnector.alignmentFocus;
          edge.addBtnSegmentIndex = FlowDiagramConstants.LINK_ADD_BTN_SEGMENT.falseConnector.segmentIndex;
          edge.addBtnSegmentFraction = FlowDiagramConstants.LINK_ADD_BTN_SEGMENT.falseConnector.segmentFraction;
      } else if (isDecisionConnector(fromNode)) {
          edge.addBtnSegmentIndex = FlowDiagramConstants.LINK_ADD_BTN_SEGMENT.decisionConnector.segmentIndex;
          edge.addBtnSegmentFraction = FlowDiagramConstants.LINK_ADD_BTN_SEGMENT.decisionConnector.segmentFraction;
          edge.hideToArrow = FlowDiagramConstants.LINK_ADD_BTN_SEGMENT.decisionConnector.hideToArrow;
          edge.addToEndSegmentLength = FlowDiagramConstants.LINK_ADD_BTN_SEGMENT.decisionConnector.addToEndSegmentLength;
          // show the edge toArrow when there are no branches
          if (!fromNode.relation || !fromNode.relation.children || fromNode.relation.children.length === 0) {
              edge.hideToArrow = false;
          }
          // if toNode is Stage then hide the + button on the edge
          if (toNode && toNode.key && FlowDiagramBuilderApi.stagesAssociatedNodeMap[toNode.key]) {
              edge.addToEndSegmentLength = 0;
              edge.addBtnVisibility = FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopLoopback;
          }
      } else if (isLoopBackConnector(edge, fromNode)) {
          edge.labelSegmentIndex = FlowDiagramConstants.LINK_LABEL_SEGMENT.loopBackConnector.segmentIndex;
          edge.labelSegmentFraction = FlowDiagramConstants.LINK_LABEL_SEGMENT.loopBackConnector.segmentFraction;
          edge.labelAlignmentFocus = FlowDiagramConstants.LINK_LABEL_SEGMENT.loopBackConnector.alignmentFocus;
      } else if (isLoopContinueConnector(edge, fromNode) || isFromPathBrokenConnector(edge, fromNode)) {
          edge.labelSegmentIndex = FlowDiagramConstants.LINK_LABEL_SEGMENT.loopContinueConnector.segmentIndex;
          edge.labelSegmentFraction = FlowDiagramConstants.LINK_LABEL_SEGMENT.loopContinueConnector.segmentFraction;
          edge.labelAlignmentFocus = FlowDiagramConstants.LINK_LABEL_SEGMENT.loopContinueConnector.alignmentFocus;
          edge.addBtnSegmentIndex = FlowDiagramConstants.LINK_ADD_BTN_SEGMENT.loopContinueConnector.segmentIndex;
          edge.addBtnSegmentFraction = FlowDiagramConstants.LINK_ADD_BTN_SEGMENT.loopContinueConnector.segmentFraction;
      } else if (isGoBackToConnector(edge)) {
          edge.labelSegmentIndex = FlowDiagramConstants.LINK_LABEL_SEGMENT.goBackToConnector.segmentIndex;
          edge.labelSegmentFraction = FlowDiagramConstants.LINK_LABEL_SEGMENT.goBackToConnector.segmentFraction;
          edge.labelAlignmentFocus = FlowDiagramConstants.LINK_LABEL_SEGMENT.goBackToConnector.alignmentFocus;
          edge.addBtnSegmentIndex = FlowDiagramConstants.LINK_LABEL_SEGMENT.goBackToConnector.segmentIndex;
          edge.addBtnSegmentFraction = FlowDiagramConstants.LINK_LABEL_SEGMENT.goBackToConnector.segmentFraction;
      } else {
          edge.addBtnSegmentIndex = FlowDiagramConstants.LINK_ADD_BTN_SEGMENT.normal.segmentIndex;
          edge.addBtnSegmentFraction = FlowDiagramConstants.LINK_ADD_BTN_SEGMENT.normal.segmentFraction;
      }
  });
};

FlowDiagramBuilderApi.addDataModelRelationship = function(flowJSON, dataModel) {
  if (!flowJSON || !flowJSON.componentInstances)
      return;
  var instances = flowJSON.componentInstances;
  var instanceRelations = {};
  var typeLookup = {};
  if (instances.length > 0) {
      var currentDepth1 = instances[0];
      instances.forEach(function(instance) {
          var key = instance.uiUniqueIdentifier;
          var children = instance.children || [];

          var selfRelation = instanceRelations[instance.uiUniqueIdentifier];
          if (!selfRelation) selfRelation = {};
          selfRelation.parent = instance.parent;
          selfRelation.children = instance.children;

          if (instance.depth === 1 && key !== currentDepth1.uiUniqueIdentifier) {
              var currentDepth1Id = currentDepth1.uiUniqueIdentifier;
              selfRelation.previousSibling = currentDepth1Id;
              var previousSibling = instanceRelations[currentDepth1Id];
              if (!previousSibling) previousSibling = {};
              previousSibling.nextSibling = key;
              instanceRelations[currentDepth1Id] = previousSibling;
              currentDepth1 = instance;
          }

          instanceRelations[instance.uiUniqueIdentifier] = selfRelation;
          for (var i = 0; i < children.length; i++) {
              var childRelation = instanceRelations[children[i]];
              if (!childRelation) childRelation = {};
              if (i > 0) {
                  childRelation.previousSibling = children[i - 1];
              }
              if (i < children.length - 1) {
                  childRelation.nextSibling = children[i + 1];
              }
              instanceRelations[children[i]] = childRelation;
              if (instance && instance.flowLogicDefinition && instance.flowLogicDefinition.type) {
                  typeLookup[key] = instance.flowLogicDefinition.type;
              }
          }
      });
  }

  dataModel.diagramJSON.nodes.forEach(function(node) {
      node.relation = instanceRelations[node.key];
  });

  function isElseIfElseNode(nodeId, instances) {
      if (!nodeId) {
          return false;
      }

      var matched;
      for (var i = 0; i < instances.length; i++) {
          if (instances[i].uiUniqueIdentifier === nodeId) {
              matched = instances[i];
              break;
          }
      }

      if (matched) {
          var flowLogicType;
          if (matched.flowLogicDefinition) {
              flowLogicType = matched.flowLogicDefinition.type;
          }
          if (
              flowLogicType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF ||
              flowLogicType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE
          ) {
              return true;
          }
      }
      return false;
  }

  var nodeMap = {};
  dataModel.diagramJSON.nodes.forEach(function(node) {
      nodeMap[node.key] = node;
  });
  dataModel.diagramJSON.nodes.forEach(function(node) {
      var endId;
      var beforeInsertion, beforeInsertionId, afterInsertion, parentInsertion;
      if (node.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP || node.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP) {
          endId = node.key;
          if (node.relation === undefined || node.relation === null) {
              node.relation = {};
          }
          // for each ENDLOOP, find the corresponding LOOP node
          var loopNodeId = node.loopStartNodeKey;
          var loopNode = nodeMap[loopNodeId];
          var loopParentId, loopNextSiblingId;
          if (loopNode.relation) {
              loopParentId = loopNode.relation.parent;
              loopNextSiblingId = loopNode.relation.nextSibling;
          }

          if (loopParentId) {
              parentInsertion = nodeMap[loopParentId];
          }
          if (loopNextSiblingId) {
              afterInsertion = nodeMap[loopNextSiblingId];
          }
          beforeInsertion = loopNode;
          beforeInsertionId = loopNode.key;
      } else if (node.instanceType === "ENDIF") {
          // for each ENDIF,
          // 1. find the last IF / ELSEIF / hidden ELSE in the corresponding IF node, and
          // 2. find the next sibling of the last IF / ELSEIF / hidden ELSE
          endId = node.key;
          if (node.relation === undefined || node.relation === null) {
              node.relation = {};
          }
          var ifNodeId = node.sourceIfNodeKey;
          var ifNode = nodeMap[ifNodeId];
          var current = ifNode;
          var ifParentId;
          if (ifNode.relation) {
              ifParentId = ifNode.relation.parent;
          }
          if (ifParentId) {
              parentInsertion = nodeMap[ifParentId];
          }

          var nextSiblingId = current.relation ?
              current.relation.nextSibling :
              undefined;
          while (isElseIfElseNode(nextSiblingId, instances)) {
              current = nodeMap[nextSiblingId];
              if (!current) {
                  // hidden ELSE
                  beforeInsertionId = nextSiblingId;
                  nextSiblingId = instanceRelations[nextSiblingId].nextSibling;
                  break;
              } else {
                  beforeInsertionId = current.key;
                  nextSiblingId = current.relation ?
                      current.relation.nextSibling :
                      undefined;
              }
          }
          // current is the last IF / ELSEIF / hidden ELSE
          // nextSibling is the next sibling of the last IF / ELSEIF / hidden ELSE
          beforeInsertion = current;
          if (beforeInsertion && !beforeInsertionId) {
              beforeInsertionId = beforeInsertion.key;
          }
          if (nextSiblingId) {
              afterInsertion = nodeMap[nextSiblingId];
          }
      }

      if (endId) {
          if (
              parentInsertion &&
              parentInsertion.relation &&
              parentInsertion.relation.children
          ) {
              var insertionIndex = parentInsertion.relation.children.indexOf(
                  beforeInsertionId
              );
              if (insertionIndex > -1) {
                  parentInsertion.relation.children.splice(
                      insertionIndex + 1,
                      0,
                      endId
                  );
              }
              node.relation.parent = parentInsertion.key;
          }
          if (afterInsertion && afterInsertion.relation) {
              afterInsertion.relation.previousSibling = endId;
              node.relation.nextSibling = afterInsertion.key;
          }
          if (beforeInsertion && beforeInsertion.relation) {
              beforeInsertion.relation.nextSibling = endId;
              node.relation.previousSibling = beforeInsertion.key;
          } else {
              node.relation.previousSibling = beforeInsertionId;
          }
      }
  });
  return nodeMap;
};

FlowDiagramBuilderApi.removeUnnecessaryFieldsOnNodes = function(nodes) {
  for (var i = 0; i < nodes.length; i++) {
      // array of ancestor FOR EACH flow logic keys
      if (nodes[i].sourceForEachKeys) {
          delete nodes[i].sourceForEachKeys;
      }
      // array of ancestor IF flow logic keys
      if (nodes[i].sourceIfKeys) {
          delete nodes[i].sourceIfKeys;
      }
      // array of IF flow logic keys that ends with the previous node
      if (nodes[i].endOfIfKeys) {
          delete nodes[i].endOfIfKeys;
      }
      // parallel block key that contains this node. parallel block node is later removed
      if (nodes[i].parallelBlockKey) {
          delete nodes[i].parallelBlockKey;
      }
      // parallel keys that ends with the previous node
      if (nodes[i].endOfParallelKeys) {
          delete nodes[i].endOfParallelKeys;
      }
      // parallel key that contains this node
      if (nodes[i].parallelKey) {
          delete nodes[i].parallelKey;
      }
      // decision key that contains this node
      if (nodes[i].decisionKey) {
          delete nodes[i].decisionKey;
      }
      // traversed keys/nodes for the path broken node
      if (nodes[i].traversedKeys) {
          delete nodes[i].traversedKeys;
      }
      // decision block key that contains this node.
      if (nodes[i].decisionBlockKey) {
          delete nodes[i].decisionBlockKey;
      }
      // decision keys that ends with the previous node
      if (nodes[i].endOfDecisionKeys) {
          delete nodes[i].endOfDecisionKeys;
      }
  }
};

FlowDiagramBuilderApi.addEndOfIfKeys = function(record, key) {
  if (record.endOfIfKeys)
      record.endOfIfKeys += ',' + key;
  else
      record.endOfIfKeys = key;
};

// processes the remaining keys for END node
FlowDiagramBuilderApi.processRemainingActiveIfKeys = function(activeIfKeyByDepthHash, totalNodes) {
  var endNode;
  var i;
  for (i = 0; i < totalNodes.length; i++) {
      if (totalNodes[i].key === FlowDiagramBuilderApi.endKey) {
          endNode = totalNodes[i];
          break;
      }
  }

  if (activeIfKeyByDepthHash && totalNodes && endNode) {
      for (var key in activeIfKeyByDepthHash) {
          if (Object.prototype.hasOwnProperty.call(activeIfKeyByDepthHash, key)) {
              var val = activeIfKeyByDepthHash[key];
              if (val && val instanceof Array) {
                  for (i = 0; i < val.length; i++) {
                      FlowDiagramBuilderApi.addEndOfIfKeys(endNode, val[i].key);
                  }
              }
          }
      }
  }
};

// remove unconnected nodes (nodes that are not connected by any edge)
// most likely a by product of ELSE implementation
FlowDiagramBuilderApi.removeUnconnectedNodes = function (
totalNodes,
totalEdges
) {
var i, j;
var hiddenNodes = [];
for (i = 0; i < totalEdges.length; i++) {
  if (FlowDiagramBuilderApi.instanceRecordsHash[totalEdges[i].to])
    FlowDiagramBuilderApi.instanceRecordsHash[totalEdges[i].to]
      .connectedCount++;
  if (FlowDiagramBuilderApi.instanceRecordsHash[totalEdges[i].from])
    FlowDiagramBuilderApi.instanceRecordsHash[totalEdges[i].from]
      .connectedCount++;
  // inner logic may have ENDIF nodes disconnected by END FLOW which do not cause a Path Broken
  if (FlowDiagramBuilderApi.hiddenRecordsHash[totalEdges[i].to] && FlowDiagramBuilderApi.hiddenRecordsHash[totalEdges[i].to].instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDIF) {
    if (!FlowDiagramBuilderApi.hiddenRecordsHash[totalEdges[i].to].connectedCount) {
      FlowDiagramBuilderApi.hiddenRecordsHash[totalEdges[i].to].connectedCount = 1;
      hiddenNodes.push(FlowDiagramBuilderApi.hiddenRecordsHash[totalEdges[i].to]);
    } else {
      FlowDiagramBuilderApi.hiddenRecordsHash[totalEdges[i].to].connectedCount += 1;
    }
  }
  if (FlowDiagramBuilderApi.hiddenRecordsHash[totalEdges[i].from] && FlowDiagramBuilderApi.hiddenRecordsHash[totalEdges[i].from].instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDIF) {
    if (!FlowDiagramBuilderApi.hiddenRecordsHash[totalEdges[i].from].connectedCount) {
      FlowDiagramBuilderApi.hiddenRecordsHash[totalEdges[i].from].connectedCount = 1;
      hiddenNodes.push(FlowDiagramBuilderApi.hiddenRecordsHash[totalEdges[i].from]);
    } else {
      FlowDiagramBuilderApi.hiddenRecordsHash[totalEdges[i].from].connectedCount += 1;
    }
  }
}

var orderedNodes = FlowDiagramBuilderApi.instanceRecordsOrderedList;
for (i = 0; i < orderedNodes.length; i++) {
  if (orderedNodes[i].connectedCount === 0) {
    for (j = 0; j < totalNodes.length; j++) {
      if (totalNodes[j].key === orderedNodes[i].key) {
        totalNodes.splice(j, 1);
        j--;
        break;
      }
    }
  }
}

// hidden nodes (ie ENDIF, ENDLOOP) must have at least 2 edges, else remove them
for (var k = 0; k < hiddenNodes.length; k++) {
  if (hiddenNodes[k].connectedCount <= 1) {
    const nodeKey = hiddenNodes[k].key;
    for (j = 0; j < totalNodes.length; j++) {
      if (totalNodes[j].key === nodeKey) {
        totalNodes.splice(j, 1);
        j--;
        break;
      }
    }
    // we also need to remove the connected edge
    for (var m = 0; m < totalEdges.length; m++) {
      if (totalEdges[m].from === nodeKey || totalEdges[m].to === nodeKey) {
        totalEdges.splice(m, 1);
        m--;
        break;
      }
    }
  }
}
};

// updatePathBrokenForParallel() should only affect PARALLEL and not DECISION
// because of PARALLEL's unique behaivior on path broken; only one branch with all paths blocked by
// END flow logic will result in path broken for all PARALLEL branches.
// Must be called after end of parallel node has been generated
// 1) sort nodes
// 2) for every parallel/decision branch, check to see if there is blocked path
// 3) if there is a blocked path, add path broken after the end of parallel node
// and move all outgoing edges from end of parallel node to the new path broken node
FlowDiagramBuilderApi.updatePathBrokenForParallel = function(totalNodes, totalEdges) {
  var i, j, k;
  var orderedNodes = FlowDiagramBuilderApi.sortNodes(totalNodes);
  var parallelKey;
  var endOfParallelKey;
  var pathBlockedForParallelData = {};
  for (i = 0; i < orderedNodes.length; i++) {
      if (orderedNodes[i] && orderedNodes[i].key) {
          if (FlowDiagramBuilderApi.getInstanceRecordHashNodeType(orderedNodes[i].key) === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLEL) {
              parallelKey = orderedNodes[i].key;
              endOfParallelKey = "";
              // find the node that contains matching endOfParallelKeys in regards to PARALLEL node's key
              // which will decide where the end of parallel node will be inserted (and connected)
              for (j = i + 1; j < orderedNodes.length; j++) {
                  if (orderedNodes[j] && orderedNodes[j].endOfParallelKeys) {
                      var currentEndOfParallelKeys = orderedNodes[j].endOfParallelKeys.join(",");
                      if (currentEndOfParallelKeys.includes(parallelKey)) {
                          endOfParallelKey = orderedNodes[j].key;
                          break;
                      }
                  }
              }
          } else if (FlowDiagramBuilderApi.getInstanceRecordHashNodeType(orderedNodes[i].key) === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK) {
              if (parallelKey && endOfParallelKey) {
                  var previousKeys = "";
                  // represent highest order node used/checked in isAllEdgesEndWithoutReachingTheEnd() recursion
                  // highestOrder is used to get another return value from isAllEdgesEndWithoutReachingTheEnd()
                  // highestOrder = resultArray[0]
                  // traversedKeys = resultArray[1]
                  var resultArray = [];
                  resultArray.push(-1);
                  resultArray.push("");
                  var allPathsBlocked = FlowDiagramBuilderApi.isAllEdgesEndWithoutReachingTheEnd(orderedNodes[i].key, previousKeys, totalEdges, resultArray, endOfParallelKey);
                  if (allPathsBlocked && !pathBlockedForParallelData[parallelKey]) {
                      pathBlockedForParallelData[parallelKey] = {
                          key: parallelKey,
                          resultArray: resultArray,
                          startKey: orderedNodes[i].key
                      };
                  }
              }
          }
      }
  }
  for (var key in pathBlockedForParallelData) {
      if (Object.prototype.hasOwnProperty.call(pathBlockedForParallelData, key)) {
          var pathBlockedParallel = pathBlockedForParallelData[key];
          if (pathBlockedParallel) {
              var pathBlockedParallelKey = pathBlockedParallel.key;
              var pathBlockedParallelResultArray = pathBlockedParallel.resultArray;
              var pathBlockedParallelStartKey = pathBlockedParallel.startKey;
              for (i = 0; i < orderedNodes.length; i++) {
                  var node = orderedNodes[i];
                  if (node &&
                      node.instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDPARALLEL &&
                      node.endParallelNodeKey === pathBlockedParallelKey) {
                      // add path broken node
                      var nodeOrderVisited = pathBlockedParallelResultArray[0]; // customOrder based on instanceRecordsOrderedList
                      // find the correct node that matches to customOrder based on arrayPosition,
                      // since orderedNodes array will contain additional nodes such as Trigger, PATHBROKEN, ENDPARALLEL, etc which are sorted to the start
                      for (var j = 0; j < orderedNodes.length; j++) {
                          var arrayPosition = orderedNodes[j].arrayPosition;
                          if (arrayPosition === nodeOrderVisited) {
                              nodeOrderVisited = orderedNodes[i];
                          }
                      }
                      var newNode = FlowDiagramBuilderApi.createPathBrokenNode(orderedNodes, nodeOrderVisited, pathBlockedParallelResultArray[1], node);
                      // move all outgoing edges from end of parallel node to the new path broken node
                      for (j = 0; j < totalEdges.length; j++) {
                          var edge = totalEdges[j];
                          if (edge.from === node.key) {
                              edge.from = newNode.key;
                              edge.fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_PATH_BROKEN_OUTPUT_SYS_ID;
                              break;
                          }
                      }
                      // when adjusting the layout of nodes, we need to keep track of which Parallel node the path broken belongs
                      newNode.endParallelNodeKey = pathBlockedParallelKey;
                      // iterate nodes from orderedNodes[i] to nodeOrderVisited and get all deletedLoopbackEdges
                      // for the all traversed nodes to create the path broken node
                      // orderedNodes[].deletedLoopbackEdges contains deleted loopback edges because of END FLOW node
                      var nextNodeOrder = pathBlockedParallelResultArray[0] + 1;
                      var deletedLoopbackEdges = [];
                      // Must use FlowDiagramBuilderApi.instanceRecordsOrderedList instead of orderedNodes for the below iteration because
                      // isAllEdgesEndWithoutReachingTheEnd's logic does not account for end of XX nodes and assign order values accordingly.
                      // Because orderedNodes includes end of parallel nodes, if orderedNodes is used for below iteration it will screw up the order iteration.
                      for (j = 0; j < FlowDiagramBuilderApi.instanceRecordsOrderedList.length; j++) {
                          if (FlowDiagramBuilderApi.instanceRecordsOrderedList[j].key === pathBlockedParallelStartKey) {
                              var currentOrderedNodeIndex = j;
                              while (currentOrderedNodeIndex < FlowDiagramBuilderApi.instanceRecordsOrderedList.length && currentOrderedNodeIndex < nextNodeOrder) {
                                  var currentOrderedNode = FlowDiagramBuilderApi.instanceRecordsOrderedList[currentOrderedNodeIndex];
                                  // deletedLoopbackEdges is only stored in FlowDiagramBuilderApi.instanceRecordsHash
                                  var record = FlowDiagramBuilderApi.instanceRecordsHash[currentOrderedNode.key];
                                  if (record && record.deletedLoopbackEdges)
                                      deletedLoopbackEdges = deletedLoopbackEdges.concat(record.deletedLoopbackEdges);
                                  currentOrderedNodeIndex++;
                              }
                              // re-add the deleted loopback edges
                              if (deletedLoopbackEdges) {
                                  for (k = 0; k < deletedLoopbackEdges.length; k++) {
                                      deletedLoopbackEdges[k].from = newNode.key;
                                      deletedLoopbackEdges[k].fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_PATH_BROKEN_OUTPUT_SYS_ID;
                                      totalEdges.push(deletedLoopbackEdges[k]);
                                  }
                              }
                              break;
                          }
                      }
                      // push the new node to totalNodes which is dataModel.diagramJSON.nodes
                      totalNodes.push(newNode);
                      break;
                  }
              }
          }
      }
  }
};

FlowDiagramBuilderApi.createPathBrokenNode = function(orderedNodes, nodeOrderVisited, traversedKeys, originNode) {
  var nodeId = FlowDiagramBuilderApi.getFlowLogicNodeId(FlowDiagramConstants.FLOW_LOGIC_TYPES.PATHBROKEN);
  // PATH BROKEN node is not defined anywhere in Flow Designer. We could have used END Node's key as PATH BROKEN node's
  // key but decided to create its own since all node keys should be unique
  // PATH BROKEN node does not exist in Flow and therefore there is no instance.order for it
  var newNode = FlowDiagramBuilderApi.createNode({
      name: FlowDiagramConstants.NODE_NAMES.PATH_BROKEN,
      node_id: nodeId,
      instanceType: FlowDiagramConstants.FLOW_LOGIC_TYPES.PATHBROKEN,
      traversedKeys: traversedKeys //resultArray[1]
  });

  newNode.sourceIfKeys = [];

  // orderedNodes[nodeOrderVisited] (which is the node before the new PATH BROKEN node) must be END flow logic node
  // in other words, path broken node can only be created after a END flow logic node
  var endNode = orderedNodes[nodeOrderVisited] || originNode;
  if (endNode) {
      newNode.key = FlowDiagramHelper.addStringToUuid(FlowDiagramConstants.FLOW_LOGIC_TYPES.PATHBROKEN, endNode.key);
      var endNodeOrder = endNode.order || endNode.customOrder;
      if (!isNaN(endNodeOrder)) {
          newNode.order = endNodeOrder;
      }
      if (endNode.depth) {
          newNode.depth = endNode.depth;
      }
      if (FlowDiagramBuilderApi.isValidArrayPosition(endNode.arrayPosition)) {
          newNode.arrayPosition = endNode.arrayPosition;
      }
  }

  // copy the last visited sourceIfKeys to newNode. These sourceIfKeys will be used to
  // determine how to connect ENDOF nodes to this newly created path broken node.
  if (orderedNodes.length > nodeOrderVisited &&
      endNode &&
      endNode.sourceIfKeys) {
      newNode.sourceIfKeys = endNode.sourceIfKeys.slice();
  }

  if (orderedNodes.length > nodeOrderVisited &&
      endNode &&
      endNode.sourceForEachKeys) {
      newNode.sourceForEachKeys = endNode.sourceForEachKeys.slice();
  }

  // add updatePath logic for PARALLEL
  var parallelKey = null;
  if (endNode) {
      if (endNode.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLEL) {
          parallelKey = endNode.key;
      } else if (endNode.parallelKey) {
          parallelKey = endNode.parallelKey;
      }
  }
  // add updatePath logic for DECISION
  var decisionKey = null;
  if (endNode) {
      if (endNode.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISION) {
          decisionKey = endNode.key;
      } else if (endNode.decisionKey) {
          decisionKey = endNode.decisionKey;
      }
  }
  FlowDiagramBuilderApi.instanceRecordsHash[newNode.key] = {
      key: newNode.key,
      type: FlowDiagramConstants.FLOW_LOGIC_TYPES.PATHBROKEN,
      deletedLoopbackEdges: null,
      connectedCount: 0,
      data: newNode.data,
      sourceIfKeys: newNode.sourceIfKeys,
      sourceForEachKeys: newNode.sourceForEachKeys,
      parallelKey: parallelKey,
      decisionKey: decisionKey,
      traversedKeys: traversedKeys //resultArray[1]
  };
  return newNode;
};

// Add Path broken if necessary
// note that path broken nodes can be added additionally in
// connectPathBrokenToNonStartPathBrokenNodeWithoutIncomingEdges() for edge cases
FlowDiagramBuilderApi.updatePathBroken = function(errors, totalNodes, totalEdges) {
  var i, j;
  var orderedNodes = FlowDiagramBuilderApi.instanceRecordsOrderedList;
  // Contains all nodes keys traversed for a valid path broken find. A node should only
  // be traversed once for a valid path broken find
  var totalTraversedKeysForValidPathBroken = "";
  var forcePathBrokenFind = false;
  var forceNodeOrderVisited = -1;

  for (i = 0; i < orderedNodes.length; i++) {
      // for the first "orphaned node" is found, update so that the next interation of PathBroken loop
      // starts with the first "orphaned node" found and if the first "orphaned node" has a PathBroken path
      // then the remaining nodes will get added after node visited in the current PathBroken loop
      // which is forced by forceNodeOrderVisited
      // when the new PathBroken loop begins with the first "orphaned node" it might not start with
      // DECISION, IF, or FOR. Therefore we set forcePathBrokenFind to bypass the check
      // PARALLEL node is separately handled in updatePathBrokenForParallel()
      if ((forcePathBrokenFind ||
              orderedNodes[i].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISION ||
              orderedNodes[i].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF ||
              orderedNodes[i].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.FOREACH ||
              orderedNodes[i].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DOUNTIL) &&
          totalTraversedKeysForValidPathBroken.includes(orderedNodes[i].key) === false) {
          forcePathBrokenFind = false;
          var localForceNodeOrderVisited = forceNodeOrderVisited;
          forceNodeOrderVisited = -1;
          // used to guard against stack overflow on recursion and record previously visited nodes/keys
          var previousKeys = "";
          // represent highest order node used/checked in isAllEdgesEndWithoutReachingTheEnd() recursion
          // highestOrder is used to get another return value from isAllEdgesEndWithoutReachingTheEnd()
          // highestOrder = resultArray[0]
          // traversedKeys = resultArray[1]
          var resultArray = [];
          resultArray.push(-1);
          resultArray.push("");
          var allPathsBlocked = FlowDiagramBuilderApi.isAllEdgesEndWithoutReachingTheEnd(orderedNodes[i].key, previousKeys, totalEdges, resultArray, null);

          // "allPathsBlocked && resultArray[0] !== -1" means that the current node did not reach END and is not connected to any END flow logic
          // which makes it not a path broken. Creation of Path broken node requires END flow logic.
          if (allPathsBlocked && resultArray[0] !== -1) {
              // We want the Path broken node to be connected [localForceNodeOrderVisited if exist or highestOrder plus 1] order node
              var nextNodeOrder = localForceNodeOrderVisited === -1 ? resultArray[0] + 1 : localForceNodeOrderVisited + 1;
              var nodeOrderVisited = localForceNodeOrderVisited === -1 ? resultArray[0] : localForceNodeOrderVisited + 1;
              // Nodes in PARALLEL (not DECISION) will be separately handled in updatePathBrokenForParallel()
              if (orderedNodes[nodeOrderVisited] && !orderedNodes[nodeOrderVisited].parallelKey) {
                  var newNode = FlowDiagramBuilderApi.createPathBrokenNode(orderedNodes, nodeOrderVisited, resultArray[1]);

                  // iterate nodes from orderedNodes[i] to nodeOrderVisited and get all deletedLoopbackEdges
                  // for the all traversed nodes to create the path broken node
                  // orderedNodes[].deletedLoopbackEdges contains deleted loopback edges because of END FLOW node
                  var deletedLoopbackEdges = [];
                  var currentOrderedNodeIndex = i;
                  var currentOrderedNode = orderedNodes[i];
                  while (currentOrderedNodeIndex < orderedNodes.length && currentOrderedNodeIndex < nextNodeOrder) {
                      if (orderedNodes[currentOrderedNodeIndex].deletedLoopbackEdges)
                          deletedLoopbackEdges = deletedLoopbackEdges.concat(orderedNodes[currentOrderedNodeIndex].deletedLoopbackEdges);
                      currentOrderedNodeIndex++;
                  }

                  var to;
                  var toPort;
                  if (nextNodeOrder >= orderedNodes.length) {
                      to = FlowDiagramBuilderApi.endKey;
                      toPort = FlowDiagramBuilderApi.endPort;
                  } else if (nextNodeOrder < orderedNodes.length && nextNodeOrder >= 0) {
                      var nextNode = orderedNodes[nextNodeOrder];

                      if ((nextNode.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.END || nextNode.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.GOBACKTO) &&
                          nextNode.edges &&
                          nextNode.edges.length === 0) {
                          // skip the node if the nextNode is an orphaned END node
                          nextNode = orderedNodes[nextNodeOrder + 1];
                      }

                      if (nextNode) {
                          to = nextNode.key;
                          toPort = FlowDiagramBuilderApi.getToPort(nextNode.type);
                      } else {
                          // signifies nextNodeOrder == orderedNodes.length and thus the next node is the END node
                          to = FlowDiagramBuilderApi.endKey;
                          toPort = FlowDiagramBuilderApi.endPort;
                      }
                  } else {
                      errors.push("Unexpected error happened during the processing of updatePathBroken()");
                  }
                  if (errors && errors.length === 0) {
                      var newEdge = FlowDiagramTranslateModel.getEdgeModel();
                      if (newEdge) {
                          newEdge = FlowDiagramBuilderApi.updateEdge(
                              newEdge, {
                                  from: newNode.key,
                                  to: to,
                                  fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_PATH_BROKEN_OUTPUT_SYS_ID,
                                  toPort: toPort,
                                  edgeId: FlowDiagramBuilderApi.edgeId,
                              }
                          );

                          totalEdges.push(newEdge);
                      }

                      // re-add the deleted loopback edges
                      if (deletedLoopbackEdges) {
                          for (j = 0; j < deletedLoopbackEdges.length; j++) {
                              deletedLoopbackEdges[j].from = newNode.key;
                              deletedLoopbackEdges[j].fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_PATH_BROKEN_OUTPUT_SYS_ID;
                              totalEdges.push(deletedLoopbackEdges[j]);
                          }
                      }

                      // push the new node to totalNodes which is dataModel.diagramJSON.nodes
                      totalNodes.push(newNode);

                      // if there is an node without any edge pointing to it after END flow logic within the nodes traversed to identify
                      // a path broken, connect the node to the newly created pathbroken node (we will call this "orphaned node")
                      var toNodeEdgesExists = {};
                      for (j = 0; j < totalEdges.length; j++) {
                          var edge = totalEdges[j];
                          toNodeEdgesExists[edge.to] = true;
                      }

                      var newIndexForUpdatePathBroken = -1;
                      // j = i + 1 so that it can check for orderedNodes[j - 1].type
                      // j < nodeOrderVisited because the below for loop does not have to process j = nodeOrderVisited
                      // since there should not be any nodes within the path broken traverse that is orphaned at the
                      // path broken traverse
                      for (j = i + 1; j < nodeOrderVisited; j++) {
                          if (orderedNodes[j - 1] &&
                              (orderedNodes[j - 1].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.END || orderedNodes[j - 1].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.GOBACKTO) &&
                              newEdge &&
                              orderedNodes[j].type !== FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE &&
                              !toNodeEdgesExists[orderedNodes[j].key]) {
                              newEdge.to = orderedNodes[j].key;
                              newEdge.toPort = FlowDiagramBuilderApi.getToPort(orderedNodes[j].type);

                              // for the first "orphaned node" is found, update so that the next interation of PathBroken loop
                              // starts with the first "orphaned node" found and if the first "orphaned node" has a PathBroken path
                              // then the remaining nodes will get added after node visited in the current PathBroken loop
                              // which is forced by forceNodeOrderVisited
                              // when the new PathBroken loop begins with the first "orphaned node" it might not start with
                              // IF, or FOR. Therefore we set forcePathBrokenFind to bypass the check
                              if (newIndexForUpdatePathBroken === -1) {
                                  newIndexForUpdatePathBroken = j - 1; // j-1 because it will be incremented 1 by loop i++ operation
                                  forcePathBrokenFind = true;
                                  forceNodeOrderVisited = nodeOrderVisited;
                              }
                          }
                      }

                      // Skip checking on path Broken for nodes that have already been visited by isAllEdgesEndWithoutReachingTheEnd
                      // unless there is "orphaned node". Start searching from "orphaned node" if exists.
                      if (newIndexForUpdatePathBroken >= 0)
                          i = newIndexForUpdatePathBroken;
                      else
                          i = nodeOrderVisited;
                      // keeps all keys traversed for path broken node
                      totalTraversedKeysForValidPathBroken += ("," + resultArray[1]);
                  }
              }
          } else {
              break;
          }
      } else {
          forcePathBrokenFind = false;
          forceNodeOrderVisited = -1;
      }

  }
};

FlowDiagramBuilderApi.insertToHashOfArray = function(hashArray, record, key) {
  if (!hashArray[key]) {
      hashArray[key] = [];
  }
  hashArray[key].push(record);
};

// Sometimes there could be a orphaned END OF node because of path broken processing
// if there is a such orphaned END OF node, find the source node and the associated path broken node
// move all edges (such as loopback) from the END OF node to the path broken node and then delete END OF node
FlowDiagramBuilderApi.processOrphanedEndOfNodeInPathBroken = function(nodes, edges) {
  var edgesTo = {};
  var edgesFrom = {};
  var pathBrokenNodes = [];
  var i, j, key;
  var nodesToDelete = {};
  for (i = 0; i < nodes.length; i++) {
      if (nodes[i].instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PATHBROKEN) {
          pathBrokenNodes.push(nodes[i]);
      }
  }
  for (i = 0; i < edges.length; i++) {
      var edge = edges[i];
      FlowDiagramBuilderApi.insertToHashOfArray(edgesTo, edge, edge.to);
      FlowDiagramBuilderApi.insertToHashOfArray(edgesFrom, edge, edge.from);
  }

  for (key in FlowDiagramBuilderApi.hiddenRecordsHash) {
      if (Object.prototype.hasOwnProperty.call(FlowDiagramBuilderApi.hiddenRecordsHash, key)) {
          var hiddenRecord = FlowDiagramBuilderApi.hiddenRecordsHash[key];
          if (hiddenRecord) {
              if (!edgesTo[hiddenRecord.key] || edgesTo[hiddenRecord.key].length <= 0) {
                  // find the path broken record that traversed the source of this hiddenRecord
                  var sourceNodeKey;
                  if (hiddenRecord.loopStartNodeKey) {
                      sourceNodeKey = hiddenRecord.loopStartNodeKey;
                  } else if (hiddenRecord.sourceIfNodeKey) {
                      sourceNodeKey = hiddenRecord.sourceIfNodeKey;
                  } else if (hiddenRecord.endParallelNodeKey) {
                      sourceNodeKey = hiddenRecord.endParallelNodeKey;
                  } else if (hiddenRecord.endDecisionNodeKey) {
                      sourceNodeKey = hiddenRecord.endDecisionNodeKey;
                  }

                  for (i = 0; i < pathBrokenNodes.length; i++) {
                      var pathBrokenNode = pathBrokenNodes[i];
                      if (sourceNodeKey &&
                          pathBrokenNode.traversedKeys &&
                          pathBrokenNode.traversedKeys.includes(sourceNodeKey)) {
                          // we have found an orphaned END/hidden node. First reaasign all continue loop edges
                          // and then remove the node
                          if (edgesFrom[hiddenRecord.key]) {
                              for (j = 0; j < edgesFrom[hiddenRecord.key].length; j++) {
                                  var edgeFromHiddenRecordKey = edgesFrom[hiddenRecord.key][j];
                                  FlowDiagramBuilderApi.removeEdgeFromEdges(edgeFromHiddenRecordKey, edges);
                              }
                          }

                          // At this point, hiddenRecord should not be connected, and therefore mark to delete the node
                          nodesToDelete[hiddenRecord.key] = hiddenRecord;
                          delete FlowDiagramBuilderApi.hiddenRecordsHash[key];
                      }
                  }
              }
          }
      }
  }

  FlowDiagramBuilderApi.deleteNodesIfKeyMatch(nodes, nodesToDelete);
};

FlowDiagramBuilderApi.updateEdgeLabels = function(edges) {
  var i;
  for (i = 0; i < edges.length; i++) {
      if (edges[i].toPort !== FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID &&
          edges[i].linkLabel === FlowDiagramConstants.LINK_LABELS.loopBackConnector) {
          if (edges[i].fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_TRUE_SYS_ID)
              edges[i].linkLabel = FlowDiagramConstants.LINK_LABELS.trueConnector;
          else if (edges[i].fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_FALSE_SYS_ID)
              edges[i].linkLabel = FlowDiagramConstants.LINK_LABELS.falseConnector;
          else
              edges[i].linkLabel = "";
      }
  }
};

FlowDiagramBuilderApi.removeUnnecessaryFieldsOnEdges = function(totalEdges) {
  if (totalEdges) {
      for (var i = 0; i < totalEdges.length; i++) {
          var edge = totalEdges[i];
          if (edge.minimumDepth) {
              delete edge.minimumDepth;
          }
      }
  }
};

FlowDiagramBuilderApi.removeUnnecessaryEdges = function(totalEdges) {
  var edgesHash = {};
  if (totalEdges) {
      for (var i = 0; i < totalEdges.length; i++) {
          var edge = totalEdges[i];
          // nested for loops and if statements can sometimes create continue edge that points to itself
          // delete it
          if (edge.linkLabel === FlowDiagramConstants.LINK_LABELS.loopContinueConnector &&
              edge.from === edge.to) {
              totalEdges.splice(i, 1);
              i--;
          }
          // remove all self pointing edges which is not a loop back edge
          else if (edge.from === edge.to &&
              edge.toPort !== FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID) {
              totalEdges.splice(i, 1);
              i--;
          }
          // remove edges with missing from or to
          else if (!edge.to || !edge.from) {
              totalEdges.splice(i, 1);
              i--;
          }
          // remove duplicated edges
          else {
              var hashKey = edge.from + '_' + edge.to + '_' + edge.fromPort + '_' + edge.toPort;
              if (!edgesHash[hashKey])
                  edgesHash[hashKey] = true;
              else {
                  totalEdges.splice(i, 1);
                  i--;
              }
          }
      }
  }
};

FlowDiagramBuilderApi.removeAddButtonOnEndEdges = function(totalEdges) {
  if (totalEdges) {
      for (var i = 0; i < totalEdges.length; i++) {
          var edge = totalEdges[i];
          if (edge.to === FlowDiagramBuilderApi.endKey) {
              edge.addBtnVisibility = FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopLoopback;
          }
      }
  }
};

FlowDiagramBuilderApi.createHiddenNode = function(totalNodes, sourceKey, parallelKey, decisionKey, nodeType, depth, addOneToFirstLastHexadecimal) {
  // add new node
  var nodeId = FlowDiagramBuilderApi.getFlowLogicNodeId(nodeType);
  // newKey will be the sourceKey reversed with dashes in tact
  var newKey = FlowDiagramHelper.reverseUuid(sourceKey);
  if (addOneToFirstLastHexadecimal) {
      newKey = FlowDiagramHelper.addOneToFirstLastHexadecimal(newKey);
  }
  // END LOOP node does not exist in Flow and therefore there is no instance.order/arrayPosition for it
  var newNode = FlowDiagramBuilderApi.createNode({
      name: nodeType,
      key: newKey,
      node_id: nodeId,
      instanceType: nodeType,
      depth: depth,
  });

  if (nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP ||
      nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP) {
      newNode.loopStartNodeKey = sourceKey;
  } else if (nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDIF) {
      newNode.sourceIfNodeKey = sourceKey;
  } else if (nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDPARALLEL) {
      newNode.endParallelNodeKey = sourceKey;
  } else if (nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.BEGINNINGPARALLEL) {
      newNode.beginningParallelNodeKey = sourceKey;
  } else if (nodeType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDECISION) {
      newNode.endDecisionNodeKey = sourceKey;
  }
  if (parallelKey) {
      newNode.parallelKey = parallelKey;
  }
  if (decisionKey) {
      newNode.decisionKey = decisionKey;
  }

  // create a deep copy; works if you do not use Dates, functions, undefined, regExp or Infinity
  FlowDiagramBuilderApi.hiddenRecordsHash[newKey] = JSON.parse(JSON.stringify(newNode));
  totalNodes.push(newNode);
  return newNode;
};

FlowDiagramBuilderApi.concatToMainArrayWithFlowDiagramType = function(instances, newInstances, type) {
  if (newInstances && newInstances instanceof Array) {
      FlowDiagramBuilderApi.assignFlowDiagramType(newInstances, type);
      var result = instances.concat(newInstances);
      return result;
  }
};

FlowDiagramBuilderApi.getActionDefinitionIdFromDatabase = function(actionType) {
  var definitionId = "";
  if (actionType) {
      if (actionType.fParentActionId)
          definitionId = actionType.fParentActionId;
      else
          definitionId = actionType.fId;
  }
  return definitionId;
};

FlowDiagramBuilderApi.getActionDefinitionIdFromUI = function(actionType) {
  var definitionId = "";
  if (actionType) {
      if (actionType.parent_action)
          definitionId = actionType.parent_action;
      else
          definitionId = actionType.id;
  }
  return definitionId;
};

FlowDiagramBuilderApi.getFlowLogicNodeId = function(type) {
  var nodeId = "";
  if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF || type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE || type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF)
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_CONDITIONAL_DIAGRAM_ACTION;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.FOREACH || type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DOUNTIL)
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_LOOP_DIAGRAM_ACTION;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.END)
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_END_ACTION;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PATHBROKEN)
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_PATH_BROKEN_ACTION;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDLOOP)
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_END_LOOP_ACTION;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDOLOOP)
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_END_DO_LOOP_ACTION;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDIF)
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_END_IF_ACTION;
  else if (Object.keys(FlowDiagramConstants.FLOW_LOGIC_TYPES.ONE_TO_ONE).toString().includes(type))
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_ONE_TO_ONE_ACTION;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDPARALLEL)
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_END_PARALLEL_ACTION;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.BEGINNINGPARALLEL)
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_BEGINNING_PARALLEL_ACTION;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLEL)
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_PARALLEL_ACTION;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK)
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_PARALLEL_BLOCK_ACTION;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISION)
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_DECISION_ACTION;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK)
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_DECISION_BLOCK_ACTION;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ENDDECISION)
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_END_DECISION_ACTION;
  else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.GOBACKTO)
      nodeId = FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_GO_BACK_TO_ACTION;
  return nodeId;
};

FlowDiagramBuilderApi.addInstance = function(errors, hierarchyStack, dataModel, instance, activeIfKeyByDepthHash) {
  if (instance && errors && errors.length === 0) {
      var rand = instance.uiUniqueIdentifier;
      var newNode;
      var definitionId;
      var nodeId;
      var name = instance.name;
      var order = instance.order;
      var arrayPosition = instance.arrayPosition;

      if (instance.flow_diagram_type === FlowDiagramConstants.INSTANCE_TYPES.ACTION_INSTANCE_TYPE) {
          if (instance.actionType) {
              definitionId = FlowDiagramBuilderApi.getActionDefinitionIdFromDatabase(instance.actionType);
              newNode = FlowDiagramBuilderApi.createNode({
                  name: FlowDiagramHelper.generateNodeName(instance),
                  key: rand,
                  node_id: FlowDiagramConstants.SYS_IDS.FLOW_ACTION_DIAGRAM_ACTION,
                  definition_sys_id: definitionId,
                  instance_sys_id: instance.id,
                  instanceType: FlowDiagramConstants.INSTANCE_TYPES.ACTION_INSTANCE_TYPE,
                  order: order,
                  depth: instance.depth,
                  data: instance.data,
                  arrayPosition: arrayPosition,
                  parent: instance.parent,
              });
          }
      } else if (instance.flow_diagram_type === FlowDiagramConstants.INSTANCE_TYPES.SUBFLOW_INSTANCE_TYPE) {
          if (instance.subFlow) {
              definitionId = instance.subflowSysId;
              newNode = FlowDiagramBuilderApi.createNode({
                  name: FlowDiagramHelper.generateNodeName(instance),
                  key: rand,
                  node_id: FlowDiagramConstants.SYS_IDS.FLOW_ACTION_DIAGRAM_ACTION,
                  definition_sys_id: definitionId,
                  instance_sys_id: instance.id,
                  instanceType: FlowDiagramConstants.INSTANCE_TYPES.SUBFLOW_INSTANCE_TYPE,
                  order: order,
                  depth: instance.depth,
                  data: instance.data,
                  arrayPosition: arrayPosition,
                  parent: instance.parent,
              });
          }
      } else if (instance.flow_diagram_type === FlowDiagramConstants.INSTANCE_TYPES.FLOW_LOGIC_INSTANCE_TYPE) {
          if (instance.flowLogicDefinition && instance.flowLogicDefinition.type) {
              var flowLogicDefinition = instance.flowLogicDefinition;
              nodeId = FlowDiagramBuilderApi.getFlowLogicNodeId(flowLogicDefinition.type);
              newNode = FlowDiagramBuilderApi.createNode({
                  name: FlowDiagramHelper.generateNodeName(instance),
                  key: rand,
                  node_id: nodeId,
                  definition_sys_id: instance.flowLogicDefinition.id,
                  instance_sys_id: instance.id,
                  instanceType: FlowDiagramConstants.INSTANCE_TYPES.FLOW_LOGIC_INSTANCE_TYPE,
                  order: order,
                  depth: instance.depth,
                  data: instance.data,
                  arrayPosition: arrayPosition,
                  parent: instance.parent
              });
          }
      } else if (instance.flow_diagram_type === FlowDiagramConstants.INSTANCE_TYPES.COMPONENT_INSTANCE_TYPE) {
          var instanceType;
          var isActionSystemLevel = false;
          if (instance.actionType) {
              nodeId = FlowDiagramConstants.SYS_IDS.FLOW_ACTION_DIAGRAM_ACTION;
              definitionId = FlowDiagramBuilderApi.getActionDefinitionIdFromUI(instance.actionType);
              instanceType = FlowDiagramConstants.INSTANCE_TYPES.ACTION_INSTANCE_TYPE;
              name = FlowDiagramHelper.generateNodeName(instance);
              if (instance.actionType.system) {
                  isActionSystemLevel = true;
              }
          } else if (instance.flowLogicDefinition) {
              instanceType = FlowDiagramConstants.INSTANCE_TYPES.FLOW_LOGIC_INSTANCE_TYPE;
              definitionId = instance.flowLogicDefinition.id;
              if (instance.flowLogicDefinition.type)
                  nodeId = FlowDiagramBuilderApi.getFlowLogicNodeId(instance.flowLogicDefinition.type);
              name = FlowDiagramHelper.generateNodeName(instance);
          } else if (instance.subFlow) {
              nodeId = FlowDiagramConstants.SYS_IDS.FLOW_ACTION_DIAGRAM_ACTION;
              definitionId = instance.subflowSysId;
              instanceType = FlowDiagramConstants.INSTANCE_TYPES.SUBFLOW_INSTANCE_TYPE;
              name = FlowDiagramHelper.generateNodeName(instance);
          }
          if (definitionId && nodeId)
              newNode = FlowDiagramBuilderApi.createNode({
                  name: name,
                  key: rand,
                  node_id: nodeId,
                  definition_sys_id: definitionId,
                  instance_sys_id: instance.id,
                  instanceType: instanceType,
                  order: order,
                  depth: instance.depth,
                  data: instance.data,
                  arrayPosition: arrayPosition,
                  parent: instance.parent,
                  orderBottom: instance.orderBottom,
                  isActionSystemLevel: isActionSystemLevel
              });
      }

      FlowDiagramBuilderApi.setParallelBlockKey(newNode, instance, rand);
      FlowDiagramBuilderApi.setDecisionBlockKey(newNode, instance, rand);
      FlowDiagramBuilderApi.addNodeAndEdges(errors, hierarchyStack, instance, newNode, dataModel, activeIfKeyByDepthHash);
  }
};

FlowDiagramBuilderApi.setParallelBlockKey = function(newNode, instance, rand) {
  // Attach currentParallelBlockKey to all nodes under the current ParallelBlock node
  // end and start a new currentParallelBlockKey if there is another ParallelBlock within same depth is previous ParallelBlock
  // end currentParallelBlockKey if the node has lower depth than existing ParallelBlock's depth.
  // Note that Parallel node cannot be nested
  if (newNode && instance) {
      if (instance.flowLogicDefinition &&
          instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK) {
          if (FlowDiagramBuilderApi.currentParallelBlockDepth === -1) {
              FlowDiagramBuilderApi.currentParallelBlockKey = rand;
              FlowDiagramBuilderApi.currentParallelBlockDepth = instance.depth;
          } else if (FlowDiagramBuilderApi.currentParallelBlockDepth === instance.depth) {
              FlowDiagramBuilderApi.currentParallelBlockKey = rand;
          }
      } else {
          if (FlowDiagramBuilderApi.currentParallelBlockKey && FlowDiagramBuilderApi.currentParallelBlockDepth > instance.depth) {
              // FlowDiagramBuilderApi.currentParallelBlockKey is present but current node's depth is lower than the PARALLEL BLOCK's depth
              // which signifies the end of FlowDiagramBuilderApi.currentParallelBlockKey
              FlowDiagramBuilderApi.currentParallelBlockKey = "";
              FlowDiagramBuilderApi.currentParallelBlockDepth = -1;
          }

          if (FlowDiagramBuilderApi.currentParallelBlockKey) {
              newNode.parallelBlockKey = FlowDiagramBuilderApi.currentParallelBlockKey;
          }
      }
  }
};

FlowDiagramBuilderApi.setDecisionBlockKey = function(newNode, instance, rand) {
  // Attach currentDecisionBlockKey to all nodes under the current DecisionBlock node
  // end and start a new currentDecisionBlockKey if there is another DecisionBlock within same depth is previous DecisionBlock
  // end currentDecisionBlockKey if the node has lower depth than existing DecisionBlock's depth.
  // Note that Decision node cannot be nested
  if (newNode && instance) {
      if (instance.flowLogicDefinition &&
          instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK) {
          if (FlowDiagramBuilderApi.currentDecisionBlockDepth === -1) {
              FlowDiagramBuilderApi.currentDecisionBlockKey = rand;
              FlowDiagramBuilderApi.currentDecisionBlockDepth = instance.depth;
          } else if (FlowDiagramBuilderApi.currentDecisionBlockDepth === instance.depth) {
              FlowDiagramBuilderApi.currentDecisionBlockKey = rand;
          }
      } else {
          if (FlowDiagramBuilderApi.currentDecisionBlockKey && FlowDiagramBuilderApi.currentDecisionBlockDepth > instance.depth) {
              // FlowDiagramBuilderApi.currentDecisionBlockKey is present but current node's depth is lower than the DECISION BLOCK's depth
              // which signifies the end of FlowDiagramBuilderApi.currentDecisionBlockKey
              FlowDiagramBuilderApi.currentDecisionBlockKey = "";
              FlowDiagramBuilderApi.currentDecisionBlockDepth = -1;
          }

          if (FlowDiagramBuilderApi.currentDecisionBlockKey) {
              newNode.decisionBlockKey = FlowDiagramBuilderApi.currentDecisionBlockKey;
          }
      }
  }
};

FlowDiagramBuilderApi.addNodeAndEdges = function(errors, hierarchyStack, instance, newNode, dataModel, activeIfKeyByDepthHash) {
  if (newNode && dataModel && dataModel.diagramJSON && dataModel.diagramJSON.edges && dataModel.diagramJSON.nodes) {
      var newEdges = [];
      var addNewNode = FlowDiagramBuilderApi.createAndUpdateEdgesAndNodes(errors, hierarchyStack, instance, newNode, dataModel.diagramJSON.edges, dataModel.diagramJSON.nodes, newEdges, activeIfKeyByDepthHash);
      if (errors.length === 0) {
          dataModel.diagramJSON.edges = dataModel.diagramJSON.edges.concat(newEdges);

          if (addNewNode && dataModel && dataModel.diagramJSON && dataModel.diagramJSON.nodes) {
              dataModel.diagramJSON.nodes.push(newNode);
          }
      }
  }
};
FlowDiagramBuilderApi.addStartEnd = function(hierarchyStack, dataModel, triggerDisplayName, existingTrigger, flowType, existingSubflowInputs) {
  var templateStart = FlowDiagramTranslateModel.getNodeModel();
  var templateEnd = FlowDiagramTranslateModel.getNodeModel();
  var startNodeLabel = '';
  var instanceType = '';
  var name = triggerDisplayName;

  if (FlowDiagramBuilderApi.templateDiagramJson && hierarchyStack && dataModel) {
      for (var i = 0; i < FlowDiagramBuilderApi.templateDiagramJson.nodes.length; i++) {
          if (FlowDiagramBuilderApi.templateDiagramJson.nodes[i].node_id === FlowDiagramConstants.SYS_IDS.FLOW_DIAGRAM_TEMPLATE_ADD_TRIGGER_NODE_ID) {
              if (flowType == 'subflow') {
                  templateStart = FlowDiagramBuilderApi.templateDiagramJson.nodes[i];
                  if (existingSubflowInputs) {
                      templateStart.node_id = FlowDiagramConstants.SYS_IDS.FLOW_DIAGRAM_TEMPLATE_SUBFLOW_INS_OUTS_NODE_ID;
                      name = FlowDiagramConstants.NODE_NAMES.INPUTS_OUTPUTS;
                  } else {
                      templateStart.node_id = FlowDiagramConstants.SYS_IDS.FLOW_DIAGRAM_TEMPLATE_ADD_A_SUBFLOW_INS_OUTS_NODE_ID;
                      name = FlowDiagramConstants.NODE_NAMES.ADD_INPUTS_OUTPUTS;
                      startNodeLabel = FlowDiagramConstants.NODE_NAMES.ADD_INPUTS_OUTPUTS;
                      templateStart.key = FlowDiagramConstants.NODE_KEYS.FLOW_DIAGRAM_ADD_SUBFLOW_INPUTS_OUTPUTS_KEY;
                  }
                  FlowDiagramBuilderApi.startKey = templateStart.key;
                  instanceType = FlowDiagramConstants.INSTANCE_TYPES.SUBFLOW_INPUTS_OUTPUTS_TYPE;
              } else {
                  templateStart = FlowDiagramBuilderApi.templateDiagramJson.nodes[i];
                  if (existingTrigger)
                      templateStart.node_id = FlowDiagramConstants.SYS_IDS.FLOW_DIAGRAM_TEMPLATE_TRIGGER_NODE_ID;
                  FlowDiagramBuilderApi.startKey = templateStart.key;
                  startNodeLabel = existingTrigger ? FlowDiagramConstants.NODE_NAMES.TRIGGER : FlowDiagramConstants.NODE_NAMES.ADD_TRIGGER;
                  instanceType = FlowDiagramConstants.INSTANCE_TYPES.TRIGGER_TYPE;
              }
          } else if (FlowDiagramBuilderApi.templateDiagramJson.nodes[i].node_id === FlowDiagramConstants.SYS_IDS.FLOW_DIAGRAM_TEMPLATE_ADD_A_NODE_NODE_ID) {
              templateEnd = FlowDiagramBuilderApi.templateDiagramJson.nodes[i];
              FlowDiagramBuilderApi.endKey = templateEnd.key;
          }
      }

      var start = FlowDiagramBuilderApi.createNode({
          name: name,
          key: existingTrigger || flowType == 'subflow' ? templateStart.key : FlowDiagramConstants.NODE_KEYS.FLOW_DIAGRAM_ADD_TRIGGER_NODE_KEY,
          node_id: templateStart.node_id,
          labelText: startNodeLabel,
          instanceType: instanceType
      });
      var end = FlowDiagramBuilderApi.createNode({
          name: FlowDiagramConstants.NODE_NAMES.ADD_NODE,
          key: templateEnd.key,
          node_id: templateEnd.node_id,
      });
      var edge = FlowDiagramBuilderApi.createFirstEdge(start, end);
      dataModel.diagramJSON.edges.push(edge);
      dataModel.diagramJSON.nodes.push(start);
      dataModel.diagramJSON.nodes.push(end);

      var edges = [];
      edges.push(edge);
      hierarchyStack.push({
          key: templateStart.key, // this can be uiUniqueIdentifier of the start node, if it exists
          parent: "",
          edges: edges,
      });
      return null;
  } else {
      var errorMessage = "FlowDiagramBuilderApi templateDiagramJson has not been assigned";
      return errorMessage;
  }
};

/**
* create a base node object using a model object.
* Fill base and add any properties from the passed in object
*
* @param nodeProperties {object}
* @return {object} - see FlowDiagramTranslateModel.getNodeModel() for base properties
*/
FlowDiagramBuilderApi.createNode = function(nodeProperties) {
  var node = FlowDiagramTranslateModel.getNodeModel();
  var keys = _combineKeys(nodeProperties, node);

  // if there is content, lets fill the node
  if (nodeProperties) {
      for (var i = 0; i < keys.length; i++) {
          var key = keys[i];
          node[key] = nodeProperties[key];
      }
  }

  return node;

  function _combineKeys(obj1, obj2) {
      var keys = typeof obj1 === 'object' ? Object.keys(obj1) : [];
      if (typeof obj2 === 'object') {
          keys.concat(Object.keys(obj2));
      }

      return keys;
  }
};

FlowDiagramBuilderApi.createFirstEdge = function(from, to) {
  var edge = FlowDiagramTranslateModel.getEdgeModel();
  if (from && to) {
      edge.from = from.key;
      edge.to = to.key;
      edge.edge_id = FlowDiagramBuilderApi.edgeId;
      edge.fromPort = FlowDiagramBuilderApi.startPort;
      edge.toPort = FlowDiagramBuilderApi.endPort;
      edge.linkLabel = FlowDiagramBuilderApi.linkLabel;
      edge.addBtnVisibility = FlowDiagramBuilderApi.addBtnVisibility;
      edge.addBtnText = FlowDiagramBuilderApi.addBtnText;
  }
  return edge;
};

FlowDiagramBuilderApi.processPreviousEdgesForLoopBack = function(instance, node, previousEdges, currentHierarchyStackRecord, newEdges, copyEdges, totalEdges, deletedLoopbackEdges, bypassParentCheck) {
  if (instance && currentHierarchyStackRecord && (instance.parent === currentHierarchyStackRecord.parent || instance.parent === currentHierarchyStackRecord.key || bypassParentCheck)) {
      var edge;
      var edges = [];
      var i;
      for (i = 0; i < previousEdges.length; i++) {
          var previousEdge;
          if (previousEdges && previousEdges instanceof Array)
              previousEdge = previousEdges[i];

          // for loopback edge should always be from the latest instance/node until they share same parent, which means end of loop or if/else if/else statement
          if (previousEdge && previousEdge.toPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID && FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.to] && FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.to].parent !== instance.parent) {
              var updateEdge = false;
              // if the previous instance was a If/ELSE IF/ELSE node, edge from FALSE port should migrate to the node connected to FALSE node and
              // edge from TRUE port should migrate to the node connected to TRUE edge
              // Don't update the edge if instance is ELSE node since it will be deleted soon
              if (instance.flowLogicDefinition && instance.flowLogicDefinition.type && instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE) {
                  updateEdge = false;
                  previousEdges.splice(i, 1);
                  i--;
                  copyEdges.push(previousEdge);
              } else if (FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.to].depth <= node.depth) {
                  // if the current node's depth is lower than previousEdge.to node's depth, node is out of the for loop that previousEdge belongs to
                  // and therefore do not update previousEdge
                  if (previousEdge.fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_FALSE_SYS_ID) {
                      if (instance.parent === currentHierarchyStackRecord.parent)
                          updateEdge = true;
                      else if (instance.parent === currentHierarchyStackRecord.key && currentHierarchyStackRecord.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE)
                          updateEdge = true;
                  } else if (previousEdge.fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_TRUE_SYS_ID) {
                      if (instance.parent === currentHierarchyStackRecord.key)
                          updateEdge = true;
                  } else
                      updateEdge = true;
              }

              // only update the continue edge if the new node is contained within the FOR EACH node that the edge points to
              // FlowDiagramBuilderApi.instanceRecordsHash[node.key] has not been initialized so use FlowDiagramBuilderApi.instanceRecordsHash[instance.parent] instead
              // FlowDiagramBuilderApi.processSourceForEachKeys() should have processed all nodes regarding sourceForEachKeys except for the current node and
              // that is the reason why to use FlowDiagramBuilderApi.instanceRecordsHash[instance.parent]
              if (updateEdge === true && instance && instance.parent && FlowDiagramBuilderApi.instanceRecordsHash[instance.parent]) {
                  var sourceForEachKeys = FlowDiagramBuilderApi.instanceRecordsHash[instance.parent].sourceForEachKeys;
                  if (sourceForEachKeys && sourceForEachKeys instanceof Array && sourceForEachKeys.length > 0) {
                      sourceForEachKeys = sourceForEachKeys.join(',');
                  } else {
                      sourceForEachKeys = "";
                  }
                  if (!sourceForEachKeys.includes(previousEdge.to)) {
                      updateEdge = false;
                  }
              }

              if (updateEdge) {
                  var fromPort;
                  if (instance.actionType || instance.subFlow)
                      fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ACTION_OUTPUT_SYS_ID;
                  else if (instance.flowLogicDefinition && instance.flowLogicDefinition.type) {
                      if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF || instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE || instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF) {
                          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_TRUE_SYS_ID;

                          if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF || instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF) {
                              // Do not create a new edge for ELSE node for loopback since it will be removed anyways when ELSE node is removed
                              // need another edge reaching to loop back
                              edge = FlowDiagramTranslateModel.getEdgeModel();
                              if (edge) {
                                  edge = FlowDiagramBuilderApi.updateEdge(
                                      edge, {
                                          from: node.key,
                                          fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_FALSE_SYS_ID,
                                          edgeId: previousEdge.edge_id,
                                          to: previousEdge.to,
                                          toPort: previousEdge.toPort,
                                          linkLabel: previousEdge.linkLabel,
                                          labelColor: previousEdge.labelColor,
                                          addBtnVisibility: previousEdge.addBtnVisibility,
                                      }
                                  );

                                  edges.push(edge);
                              }
                          }

                      } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.FOREACH || instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DOUNTIL) {
                          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_OUTPUT_SYS_ID;
                      } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.END) {
                          if (node.node_id === FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_END_ACTION) {
                              var instanceParent = FlowDiagramBuilderApi.instanceRecordsHash[instance.parent];
                              if (instanceParent) {
                                  if (instanceParent.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.FOREACH || instanceParent.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DOUNTIL) {
                                      fromPort = ""; // fromPort should never by ""; this must be overwritten by incoming PATH BROKEN node
                                  }
                                  // if edge that points to FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID is about to be attached
                                  // to END node, and parent exists, then delete the edge
                                  previousEdges.splice(i, 1);
                                  FlowDiagramBuilderApi.removeEdgeFromEdges(previousEdge, newEdges);
                                  FlowDiagramBuilderApi.removeEdgeFromEdges(previousEdge, totalEdges);
                                  deletedLoopbackEdges.push(previousEdge);
                                  continue;
                              }
                          } else if (node.node_id === FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_PATH_BROKEN_ACTION)
                              fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_PATH_BROKEN_OUTPUT_SYS_ID;
                      } else if (Object.keys(FlowDiagramConstants.FLOW_LOGIC_TYPES.ONE_TO_ONE).toString().includes(instance.flowLogicDefinition.type)) {
                          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_ONE_OUTPUT;
                      } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLEL) {
                          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_MANY_OUTPUT;
                      } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK) {
                          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_PARALLELBLOCK_OUTPUT_SYS_ID;
                      } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISION) {
                          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_MANY_OUTPUT;
                      } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK) {
                          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_DECISIONBLOCK_OUTPUT_SYS_ID;
                      } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.GOBACKTO) {
                          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_GOBACKTO_OUTPUT_SYS_ID;
                      }
                  }
                  previousEdge.fromPort = fromPort;
                  previousEdge.from = node.key;

                  // remove the edge from the previousEdges and add it to edges on new node
                  previousEdges.splice(i, 1);
                  i--;
                  copyEdges.push(previousEdge);
              }
          }
      }

      if (edges && copyEdges && newEdges && copyEdges instanceof Array && newEdges instanceof Array) {
          for (i = 0; i < edges.length; i++) {
              copyEdges.push(edges[i]);
              newEdges.push(edges[i]);
          }
      }
  }
};

FlowDiagramBuilderApi.removeEdgeFromEdges = function(edgeToRemove, edges) {
  if (edges && edgeToRemove) {
      for (var i = 0; i < edges.length; i++) {
          if (edges[i].from === edgeToRemove.from && edges[i].to === edgeToRemove.to && edges[i].edge_id && edgeToRemove.edge_id && edges[i].fromPort === edgeToRemove.fromPort && edges[i].toPort === edgeToRemove.toPort) {
              edges.splice(i, 1);
              i--;
          }
      }
  }
};

FlowDiagramBuilderApi.removeEdgeCompletely = function(edgeToRemove, hierarchyStack, edgesList) {
  var i;
  if (edgesList) {
      for (i = 0; i < edgesList.length; i++)
          FlowDiagramBuilderApi.removeEdgeFromEdges(edgeToRemove, edgesList[i]);
  }
  if (hierarchyStack) {
      for (i = 0; i < hierarchyStack.length; i++)
          FlowDiagramBuilderApi.removeEdgeFromEdges(edgeToRemove, hierarchyStack[i].edges);
  }
};


// update if the previousEdge is on FALSE edge and previousEdge is from the node that replaces the ELSE node which is parent of instance
// (if previousEdge connects from FALSE port and instance.parent's immediate IF is same as
// previousEdge.from's immediage IF then the two must be connected to same IF or
// instance.parent is a ELSE connected to previousEdge.from which is a IF and therefore do not update previousEdge)
// or else if do not update edge if minimumDepth is specified and instance.depth is bigger than minimumDepth
// because previousEdge should not be updated by the instance with higher depth
FlowDiagramBuilderApi.processUpdateEdgeForElseChildrenAndMinimumDepth = function(updateEdge, instance, previousEdge, newTargetKey) {
  if (FlowDiagramBuilderApi.elseReplacementHash[instance.parent] &&
      previousEdge.fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_FALSE_SYS_ID &&
      FlowDiagramBuilderApi.elseReplacementHash[instance.parent].key === previousEdge.from) {
      updateEdge = FlowDiagramBuilderApi.processUpdateEdgeBasedOnImmediateSourceKey(instance, previousEdge);
      FlowDiagramBuilderApi.updateNewTargetNodeKeyIfUpdateEdge(updateEdge, instance, newTargetKey);
  } else if (previousEdge.minimumDepth && instance.depth > previousEdge.minimumDepth) {
      updateEdge = false;
  }
  return updateEdge;
};

FlowDiagramBuilderApi.updateNewTargetNodeKeyIfUpdateEdge = function(updateEdge, instance, newTargetKey) {
  // if the edge needs to be updated, store newTargetKey within the ELSE node
  // in this case newTargetKey is node's key next to ELSE node and all edges pointing to the ELSE node should point to newTargetKey
  if (updateEdge && instance && FlowDiagramBuilderApi.elseReplacementHash && FlowDiagramBuilderApi.elseReplacementHash[instance.parent]) {
      FlowDiagramBuilderApi.elseReplacementHash[instance.parent].newTargetNodeKey = newTargetKey;
  }
};

FlowDiagramBuilderApi.processUpdateEdgeBasedOnImmediateSourceKey = function(instance, previousEdge) {
  var updateEdge = true;
  // if previousEdge connects from FALSE port and instance.parent's immediate IF is same as
  // previousEdge.from's immediage IF then the two must be connected to same IF or
  // instance.parent is a ELSE connected to previousEdge.from which is a IF and therefore do not update previousEdge
  var previousEdgeFromImmediateSourceKey = FlowDiagramBuilderApi.getImmediateSourceIfKeyFromNodeKey(previousEdge.from);
  var instanceParentImmediateSourceKey = FlowDiagramBuilderApi.getImmediateSourceIfKeyFromNodeKey(instance.parent);
  if (previousEdgeFromImmediateSourceKey &&
      instanceParentImmediateSourceKey &&
      previousEdgeFromImmediateSourceKey !== instanceParentImmediateSourceKey) {
      updateEdge = false;
  }
  return updateEdge;
};

FlowDiagramBuilderApi.createAndUpdateEdgesAndNodes = function(errors, hierarchyStack, instance, node, totalEdges, totalNodes, newEdges, activeIfKeyByDepthHash) {
  var i, j, key;
  var copyEdges = [];
  var previousEdges = [];
  var previousEdge;
  var toPort = "";
  var deletedLoopbackEdges = [];
  var addNewNode = true;
  var instanceType;
  var updateEdge;
  if (instance && hierarchyStack && node && errors && activeIfKeyByDepthHash) {
      if (instance.actionType || instance.subFlow) {
          toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ACTION_INPUT_SYS_ID;
          instanceType = FlowDiagramConstants.INSTANCE_TYPES.ACTION_INSTANCE_TYPE;
      } else if (instance.flowLogicDefinition && instance.flowLogicDefinition.type) {
          if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF || instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE || instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF) {
              toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_INPUT_SYS_ID;
              instanceType = instance.flowLogicDefinition.type;
          } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.FOREACH || instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DOUNTIL) {
              toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_NORMAL_SYS_ID;
              instanceType = instance.flowLogicDefinition.type;
          } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.END) {
              if (node.node_id === FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_END_ACTION) {
                  toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_END_INPUT_SYS_ID;
                  instanceType = FlowDiagramConstants.FLOW_LOGIC_TYPES.END;
              } else if (node.node_id === FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_PATH_BROKEN_ACTION) {
                  toPort = "";
                  instanceType = FlowDiagramConstants.FLOW_LOGIC_TYPES.PATHBROKEN;
              }
          } else if (Object.keys(FlowDiagramConstants.FLOW_LOGIC_TYPES.ONE_TO_ONE).toString().includes(instance.flowLogicDefinition.type)) {
              toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_ONE_INPUT;
              instanceType = instance.flowLogicDefinition.type;
          } else if (Object.keys(FlowDiagramConstants.FLOW_LOGIC_TYPES.ONE_TO_MANY).toString().includes(instance.flowLogicDefinition.type)) {
              toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_MANY_INPUT;
              instanceType = instance.flowLogicDefinition.type;
          } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK) {
              toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_PARALLELBLOCK_INPUT_SYS_ID;
              instanceType = instance.flowLogicDefinition.type;
          } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK) {
              toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_DECISIONBLOCK_INPUT_SYS_ID;
              instanceType = instance.flowLogicDefinition.type;
          } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.GOBACKTO) {
              toPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_GOBACKTO_INPUT_SYS_ID;
              instanceType = instance.flowLogicDefinition.type;
          }
      }

      for (i = hierarchyStack.length - 1; i >= 0; i--) {
          var edgesProcessedForLoopback = false;
          // when poping on hierarchyStack if the last node in the hierarchyStack is same as instance's parent, then last node is IF/ELSE IF/ELSE
          // set "toPort, to" accordingly and do not pop the node because the last node's FALSE edge is not ready to be updated; the FALSE
          // edge shall be updated when the node to be popped is not the instance.parent
          //  there is no need to pop left over nodes in hierarchyStack because all nodes point to END node in default
          if (instance.parent === hierarchyStack[i].key) {
              // if "instance.parent === hierarchyStack[i].key" is true, it means that the current instance's
              // parent equals to the hierarchyStack[i], which means that hierarchyStack[i] is either FOR or IF/ELSE IF/ELSE node that directly
              // contains the instance
              updateEdge = true;
              if (i === hierarchyStack.length - 1) {
                  previousEdges = hierarchyStack[i].edges;
                  FlowDiagramBuilderApi.processPreviousEdgesForLoopBack(instance, node, previousEdges, hierarchyStack[i], newEdges, copyEdges, totalEdges, deletedLoopbackEdges, false);
                  edgesProcessedForLoopback = true;

                  for (j = 0; j < previousEdges.length; j++) {
                      previousEdge = previousEdges[j];
                      if ((previousEdge.from === hierarchyStack[i].key &&
                              (previousEdge.fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_TRUE_SYS_ID ||
                                  previousEdge.fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_OUTPUT_SYS_ID)) ||
                          (previousEdge.fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_FALSE_SYS_ID &&
                              hierarchyStack[i].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE)) {

                          updateEdge = FlowDiagramBuilderApi.processUpdateEdgeForElseChildrenAndMinimumDepth(updateEdge, instance, previousEdge, node.key);

                          if (updateEdge) {
                              previousEdge.toPort = toPort;
                              previousEdge.to = node.key;
                              previousEdges.splice(j, 1);
                              j--;
                          }
                      } else if (previousEdge.from === hierarchyStack[i].key && hierarchyStack[i].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK) {
                          previousEdge.toPort = toPort;
                          previousEdge.to = node.key;
                          previousEdges.splice(j, 1);
                          j--;
                      } else if (previousEdge.from === hierarchyStack[i].key &&
                          previousEdge.toPort === FlowDiagramBuilderApi.endPort &&
                          hierarchyStack[i].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLEL &&
                          instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK) {
                          // if the previous node is FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLEL
                          // and currentnode is FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK
                          // and previousEdge's toPort is end of the flow, delete the edge since it will be replaced by an edge created between
                          // PARALLEL and PARALLELBLOCK when PARALLELBLOCK is created
                          FlowDiagramBuilderApi.removeEdgeCompletely(previousEdge, null, [newEdges, totalEdges]);
                          previousEdges.splice(j, 1);
                          j--;
                      } else if (previousEdge.from === hierarchyStack[i].key && hierarchyStack[i].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK) {
                          previousEdge.toPort = toPort;
                          previousEdge.to = node.key;
                          previousEdges.splice(j, 1);
                          j--;
                      } else if (previousEdge.from === hierarchyStack[i].key &&
                          previousEdge.toPort === FlowDiagramBuilderApi.endPort &&
                          hierarchyStack[i].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISION &&
                          instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK) {
                          // if the previous node is FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISION
                          // and currentnode is FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK
                          // and previousEdge's toPort is end of the flow, delete the edge since it will be replaced by an edge created between
                          // DECISION and DECISIONBLOCK when DECISIONBLOCK is created
                          FlowDiagramBuilderApi.removeEdgeCompletely(previousEdge, null, [newEdges, totalEdges]);
                          previousEdges.splice(j, 1);
                          j--;
                      } else if (previousEdge.from === hierarchyStack[i].key && hierarchyStack[i].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.GOBACKTO) {
                          previousEdge.toPort = toPort;
                          previousEdge.to = node.key;
                          previousEdges.splice(j, 1);
                          j--;
                      }
                  }
              } else {
                  previousEdges = hierarchyStack[i].edges;
                  for (j = 0; j < previousEdges.length; j++) {
                      previousEdge = previousEdges[j];
                      updateEdge = FlowDiagramBuilderApi.processUpdateEdgeForElseChildrenAndMinimumDepth(updateEdge, instance, previousEdge, node.key);

                      if (updateEdge) {
                          previousEdge.toPort = toPort;
                          previousEdge.to = node.key;
                      }
                  }
              }
              // iterate through hierarchyStack until instance's parent match is found
              break;
          } else if (instance.parent === hierarchyStack[i].parent) {
              // if "instance.parent === hierarchyStack[i].parent" is true, it means that the current instance's is at the
              // same hierarchical depth with hierarchyStack[i]
              previousEdges = hierarchyStack[i].edges;
              FlowDiagramBuilderApi.processPreviousEdgesForLoopBack(instance, node, previousEdges, hierarchyStack[i], newEdges, copyEdges, totalEdges, deletedLoopbackEdges, false);
              edgesProcessedForLoopback = true;
          }

          previousEdges = hierarchyStack[i].edges;
          for (j = 0; j < previousEdges.length; j++) {
              previousEdge = previousEdges[j];
              if (previousEdge.toPort !== FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID) {
                  updateEdge = true;
                  updateEdge = FlowDiagramBuilderApi.processUpdateEdgeForElseChildrenAndMinimumDepth(updateEdge, instance, previousEdge);
                  if (updateEdge) {
                      if (hierarchyStack[i] &&
                          instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF) {
                          // below if and else if statements are used for when current node is ELSE IF and previous node's IF node TRUE edge
                          // or ELSE IF node TRUE edge don't get connected to current node
                          // if hierarchyStack[i] is previous node and IF or ELSE IF node and current node is ELSE IF and (
                          // if the previousEdge's depth is same as instance's depth and the edge is FALSE edge and
                          // the previousEdge's type is either ELSE or IF, then previousEdge must be the edge that connects main IF or ELSE IF
                          // flow logic to the current node (which is a ELSE IF) and therefore update. Otherwise, don't update)
                          if ((hierarchyStack[i].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF ||
                                  hierarchyStack[i].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF)) {
                              if (!(FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.from].depth === instance.depth &&
                                      previousEdge.fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_FALSE_SYS_ID &&
                                      (FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.from].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF ||
                                          FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.from].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF))) {
                                  updateEdge = false;
                              }
                          }
                          // if hierarchyStack[i] is previous node and  hierarchyStack[i]'s parent type is IF or ELSE IF node and
                          // current node is ELSE IF and the edge is coming from TRUE, don't update.
                          else if (FlowDiagramBuilderApi.instanceRecordsHash[hierarchyStack[i].parent]) {
                              var previousNodeParentType = FlowDiagramBuilderApi.instanceRecordsHash[hierarchyStack[i].parent].type;
                              if (previousNodeParentType === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF ||
                                  previousNodeParentType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF)
                                  updateEdge = false;
                          }
                      }
                      // if instance is ELSE IF and if there is any parent of hierarchyStack[i] that is IF or ELSE IF that shares the same parent of the instance
                      // and previousEdge is not a FALSE edge, then don't update.
                      // As long as the instance is ELSE IF then the previousEdge won't be updated. However, if instance of anything other than
                      // ELSE IF then the previousEdge will be updated. Remember that not all nodes will be used as instance to process upon a particular
                      // previousEdge, which will be based on how hierarchyStack was implemented
                      // previousEdge as FALSE edge needs to be updated and connected to upcoming ELSE IF or ELSE node if the previousEdge and currentHierachyStack are in same depth

                      // if currentHierachyStack.depth < FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.from],
                      // then previousEdge.from must be on immediate "true" edge path of currentHierachyStack
                      // in other words, if FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.from],
                      // and currentHierachyStack is IF or ELSE IF, only way for previousEdge.from to have bigger depth number than currentHierachyStack
                      // is to be part of its true edge heritage and its false edge heritage cannot have bigger depth number
                      // therefore we treat previousEdge as it is attached to true edge because it stemmed from immediate true edge of currentHierachyStack
                      // therefore bypassing the previousEdge.fromPort !== FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_FALSE_SYS_ID
                      //  if currentHierachyStack is an ancestor of previousEdge
                      var currentHierachyStack = hierarchyStack[i];
                      var previousEdgeFrom = "";
                      if (previousEdge && FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.from]) {
                          previousEdgeFrom = FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.from];
                      }
                      if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF) {
                          while (currentHierachyStack && instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF) {
                              if ((currentHierachyStack.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF ||
                                      previousNodeParentType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF) &&
                                  currentHierachyStack.parent === instance.parent &&
                                  previousEdgeFrom &&
                                  (previousEdge.fromPort !== FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_FALSE_SYS_ID ||
                                      currentHierachyStack.depth < previousEdgeFrom.depth)) {
                                  updateEdge = false;
                                  break;
                              }
                              currentHierachyStack = FlowDiagramBuilderApi.instanceRecordsHash[currentHierachyStack.parent];
                          }
                          // DEF0346001: edge from node PARALLELBLOCK is being connected to ELSEIF
                          // do not update previousEdge if previousEdge.from is within a PARALLELBLOCK or DECISIONBLOCK
                          if (previousEdgeFrom && (previousEdgeFrom.parallelKey || previousEdgeFrom.parallelBlockKey || previousEdgeFrom.decisionKey || previousEdgeFrom.decisionBlockKey || previousEdgeFrom.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISION || previousEdgeFrom.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK)) {
                              updateEdge = false;
                          }
                      }

                      // do not update previousEdge if previousEdge.from is PARALLEL node or previousEdge.to is PARALLELBLOCK node because
                      // these edges should not be altered (PARALLEL node always point to PARALLELBLOCK, PARALLELBLOCK always comes from PARALLEL)
                      if (FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.from] &&
                          FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.from].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLEL &&
                          FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.to] &&
                          FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.to].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK)
                          updateEdge = false;
                      else if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK)
                          updateEdge = false; // do not connect any currently existing edge to PARALLELBLOCK.
                      // do not update previousEdge if previousEdge.from is DECISION node or previousEdge.to is DECISIONBLOCK node because
                      // these edges should not be altered (DECISION node always point to DECISIONBLOCK, DECISIONBLOCK always comes from DECISION)
                      else if (FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.from] &&
                          FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.from].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISION &&
                          FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.to] &&
                          FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.to].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK)
                          updateEdge = false;
                      else if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK)
                          updateEdge = false; // do not connect any currently existing edge to DECISIONBLOCK.
                  }

                  // if the previousEdge will not be update or instance is ELSE (then instance will be removed by processElseNode())
                  // if instance.depth is smaller than previousEdge, previousEdge's fromPort is IF_ELSE_OUTPUT_FALSE,
                  // then the future instance that updates the previousEdge will need depth equal or smaller to
                  // instance.depth and the value will be stored as minimumDepth.
                  var previousEdgeFromRecord = FlowDiagramBuilderApi.instanceRecordsHash[previousEdge.from];
                  if (updateEdge) {
                      previousEdge.toPort = toPort;
                      previousEdge.to = node.key;

                      // Process loopback edges if it has not been process for hierarchyStack[i]
                      if (edgesProcessedForLoopback === false) {
                          FlowDiagramBuilderApi.processPreviousEdgesForLoopBack(instance, node, previousEdges, hierarchyStack[i], newEdges, copyEdges, totalEdges, deletedLoopbackEdges, true);
                          edgesProcessedForLoopback = true;
                      }

                      if (previousEdgeFromRecord &&
                          previousEdgeFromRecord.depth > instance.depth &&
                          previousEdge.fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_FALSE_SYS_ID &&
                          instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE)
                          previousEdge.minimumDepth = instance.depth;

                  } else {
                      if (previousEdgeFromRecord &&
                          previousEdgeFromRecord.depth > instance.depth &&
                          previousEdge.fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_FALSE_SYS_ID)
                          previousEdge.minimumDepth = instance.depth;

                      // move the edge to the current node so that it can be properly updated later
                      copyEdges.push(previousEdge);
                      previousEdges.splice(j, 1);
                      j--;
                  }
              }
          }

          hierarchyStack.splice(i, 1);
      }

      FlowDiagramBuilderApi.addNewEdges(instance, node, newEdges, copyEdges, hierarchyStack, totalNodes);

      // set the deletedLoopbackEdges so that it could be added back later during path broken implementation
      if ((instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.END || instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.GOBACKTO) && FlowDiagramBuilderApi.instanceRecordsHash[node.key])
          FlowDiagramBuilderApi.instanceRecordsHash[node.key].deletedLoopbackEdges = deletedLoopbackEdges;
      else if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE)
          addNewNode = FlowDiagramBuilderApi.processElseNode(errors, hierarchyStack, instance, node, totalEdges, totalNodes, newEdges);

      if (FlowDiagramBuilderApi.instanceRecordsHash[instance.parent] &&
          (FlowDiagramBuilderApi.instanceRecordsHash[instance.parent].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.FOREACH ||
              FlowDiagramBuilderApi.instanceRecordsHash[instance.parent].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DOUNTIL) &&
          instanceType !== FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE &&
          instanceType !== FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF) {
          FlowDiagramBuilderApi.processLoopback(instanceType, instance, node, copyEdges, newEdges, totalEdges, hierarchyStack);
      }
      FlowDiagramBuilderApi.processEndOfIfKeys(activeIfKeyByDepthHash, node, instance, instanceType);
      FlowDiagramBuilderApi.processSourceIfKeys(node, instance, instanceType);
      FlowDiagramBuilderApi.processSourceForEachKeys(node, instance, instanceType);
      FlowDiagramBuilderApi.processSourceParallelKeys(node, instance, instanceType);

      if (addNewNode &&
          (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF || instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF)) {
          FlowDiagramBuilderApi.previousIfOrElseIfKey = node.key;
      }
  } else
      errors.push("Error! instance or hierarchyStack or node in FlowDiagramBuilderApi.createAndUpdateEdgesAndNodes is null");
  return addNewNode;
};

FlowDiagramBuilderApi.processSourceParallelKeys = function(node, instance, instanceType) {
  if (node && node.key) {
      if (!FlowDiagramBuilderApi.instanceRecordsHash[node.key]) {
          FlowDiagramBuilderApi.instanceRecordsHash[node.key] = {};
      }
      if (node.parallelBlockKey) {
          FlowDiagramBuilderApi.instanceRecordsHash[node.key].parallelBlockKey = node.parallelBlockKey;
      }
      if (node && node.parallelKey) {
          FlowDiagramBuilderApi.instanceRecordsHash[node.key].parallelKey = node.parallelKey;
      }
  }
};


FlowDiagramBuilderApi.getSourceIfKeysFromNodeKey = function(nodeKey) {
  if (FlowDiagramBuilderApi.instanceRecordsHash[nodeKey] &&
      FlowDiagramBuilderApi.instanceRecordsHash[nodeKey].sourceIfKeys)
      return FlowDiagramBuilderApi.instanceRecordsHash[nodeKey].sourceIfKeys;
  else
      return null;
};

FlowDiagramBuilderApi.getImmediateSourceIfKeyFromNodeKey = function(nodeKey) {
  var result = null;
  var sourceIfKeys = FlowDiagramBuilderApi.getSourceIfKeysFromNodeKey(nodeKey);
  if (sourceIfKeys && sourceIfKeys instanceof Array && sourceIfKeys.length > 0) {
      return sourceIfKeys[sourceIfKeys.length - 1];
  }
  return null;
};

FlowDiagramBuilderApi.getImmediateSourceIfKeyFromNode = function(node) {
  if (node && node.sourceIfKeys) {
      var sourceIfKeys = node.sourceIfKeys;
      if (sourceIfKeys && sourceIfKeys instanceof Array && sourceIfKeys.length > 0) {
          return sourceIfKeys[sourceIfKeys.length - 1];
      }
  }
  return null;
};

FlowDiagramBuilderApi.getImmediateSourceIfKeyNodeFromNodeKey = function(nodeKey) {
  var resultKey = FlowDiagramBuilderApi.getImmediateSourceIfKeyFromNodeKey(nodeKey);
  if (resultKey && FlowDiagramBuilderApi.instanceRecordsHash[resultKey]) {
      return FlowDiagramBuilderApi.instanceRecordsHash[resultKey];
  }
  return null;
};

FlowDiagramBuilderApi.getImmediateSourceIfKeyNodeFromNode = function(node) {
  var resultKey = FlowDiagramBuilderApi.getImmediateSourceIfKeyFromNode(node);
  if (resultKey && FlowDiagramBuilderApi.instanceRecordsHash[resultKey]) {
      return FlowDiagramBuilderApi.instanceRecordsHash[resultKey];
  }
  return null;
};

FlowDiagramBuilderApi.copyKeys = function(keys) {
  var newKeys = [];
  if (keys instanceof Array) {
      for (var i = 0; i < keys.length; i++) {
          newKeys.push(keys[i]);
      }
  }
  return newKeys;
};

FlowDiagramBuilderApi.processSourceForEachKeys = function(node, instance, instanceType) {
  if (!node.sourceForEachKeys)
      node.sourceForEachKeys = [];
  if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.FOREACH || instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DOUNTIL) {
      if (instance.parent && FlowDiagramBuilderApi.instanceRecordsHash[instance.parent] && FlowDiagramBuilderApi.instanceRecordsHash[instance.parent].sourceForEachKeys) {
          node.sourceForEachKeys = FlowDiagramBuilderApi.copyKeys(FlowDiagramBuilderApi.instanceRecordsHash[instance.parent].sourceForEachKeys);
      }
      node.sourceForEachKeys.push(node.key);
      FlowDiagramBuilderApi.instanceRecordsHash[node.key].sourceForEachKeys = node.sourceForEachKeys;
  } else if (instance.parent && FlowDiagramBuilderApi.instanceRecordsHash[instance.parent] && FlowDiagramBuilderApi.instanceRecordsHash[instance.parent].sourceForEachKeys) {
      // if the node is not FOR EACH node then get sourceForEach value from the parent if exists
      node.sourceForEachKeys = FlowDiagramBuilderApi.copyKeys(FlowDiagramBuilderApi.instanceRecordsHash[instance.parent].sourceForEachKeys);
      FlowDiagramBuilderApi.instanceRecordsHash[node.key].sourceForEachKeys = node.sourceForEachKeys;
  }
};

// saves the nearest IF node key value to the node if the node is part of a IF statement into the array sourceIfKeys
// sourceIfKeys will contain all IF ancestors containing the node in order
// sourceIfsByDepthHash will always contain the most immediate IF node on the depth level
FlowDiagramBuilderApi.processSourceIfKeys = function(node, instance, instanceType) {
  if (!node.sourceIfKeys)
      node.sourceIfKeys = [];
  if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF) {
      if (instance.parent && FlowDiagramBuilderApi.instanceRecordsHash[instance.parent] && FlowDiagramBuilderApi.instanceRecordsHash[instance.parent].sourceIfKeys) {
          node.sourceIfKeys = FlowDiagramBuilderApi.copyKeys(FlowDiagramBuilderApi.instanceRecordsHash[instance.parent].sourceIfKeys);
      }
      node.sourceIfKeys.push(node.key);
      FlowDiagramBuilderApi.instanceRecordsHash[node.key].sourceIfKeys = node.sourceIfKeys;
      FlowDiagramBuilderApi.sourceIfsByDepthHash[instance.depth] = node;
  } else if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF || instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE) {
      if (FlowDiagramBuilderApi.sourceIfsByDepthHash[instance.depth]) {
          if (instance.parent && FlowDiagramBuilderApi.instanceRecordsHash[instance.parent] && FlowDiagramBuilderApi.instanceRecordsHash[instance.parent].sourceIfKeys) {
              node.sourceIfKeys = FlowDiagramBuilderApi.copyKeys(FlowDiagramBuilderApi.instanceRecordsHash[instance.parent].sourceIfKeys);
          }

          node.sourceIfKeys = node.sourceIfKeys.concat(FlowDiagramBuilderApi.copyKeys(FlowDiagramBuilderApi.sourceIfsByDepthHash[instance.depth].sourceIfKeys));
          FlowDiagramBuilderApi.instanceRecordsHash[node.key].sourceIfKeys = node.sourceIfKeys;
      }
  } else if (instance.parent && FlowDiagramBuilderApi.instanceRecordsHash[instance.parent] && FlowDiagramBuilderApi.instanceRecordsHash[instance.parent].sourceIfKeys) {
      // if the node is not IF/ELSE IF/ELSE node then get sourceIf value from the parent if exists
      node.sourceIfKeys = FlowDiagramBuilderApi.copyKeys(FlowDiagramBuilderApi.instanceRecordsHash[instance.parent].sourceIfKeys);
      FlowDiagramBuilderApi.instanceRecordsHash[node.key].sourceIfKeys = node.sourceIfKeys;
  }
};

// do not update endOfIfKeys if the new node is PARALLELBLOCK or the new node is in different PARALLELBLOCK
// compared to source if's PARALLELBLOCK
FlowDiagramBuilderApi.requireUpdateOnEndOfIfKeys = function(activeIfKeyData, node, instanceType) {
  var update = true;
  if ((activeIfKeyData.parallelBlockKey &&
          node.parallelBlockKey &&
          activeIfKeyData.parallelBlockKey != node.parallelBlockKey) ||
      instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK) {
      update = false;
  } else if ((activeIfKeyData.decisionBlockKey &&
          node.decisionBlockKey &&
          activeIfKeyData.decisionBlockKey != node.decisionBlockKey) ||
      instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK) {
      update = false;
  }
  return update;
};

// collects all IF node key values that ends on a node and marks the node with all IF node key values that they end with
// used for creating END IF nodes
FlowDiagramBuilderApi.processEndOfIfKeys = function(activeIfKeyByDepthHash, node, instance, instanceType) {
  var i;
  var minimumDepth;
  var activeIfKeyArray;
  var activeIfKeyData;

  // Check for activeIfKeyByDepthHash records with same value as instance.value. If there is such record, then check the current
  // instance and signify END OF IF statement if the current instanceType is not ELSE IF nor ELSE
  if (activeIfKeyByDepthHash[instance.depth] && activeIfKeyByDepthHash[instance.depth] instanceof Array) {
      activeIfKeyArray = activeIfKeyByDepthHash[instance.depth];
      for (i = 0; i < activeIfKeyArray.length; i++) {
          activeIfKeyData = activeIfKeyArray[i];
          minimumDepth = activeIfKeyData.mininumDepth;

          if (instance.depth <= minimumDepth &&
              !(instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF ||
                  instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE)) {
              if (FlowDiagramBuilderApi.requireUpdateOnEndOfIfKeys(activeIfKeyData, node, instanceType)) {
                  if (node.endOfIfKeys)
                      node.endOfIfKeys += ',' + activeIfKeyData.key;
                  else
                      node.endOfIfKeys = activeIfKeyData.key;

                  activeIfKeyArray.splice(i, 1);
                  i--;
              }
          }
      }
  }
  // iterate through activeIfKeyByDepthHash for any records that is bigger depth
  // if instance is ELSE or ELSE IF than attach the records with bigger depth to activeIfKeyByDepthHash[instance.depth]
  // otherwise signify END OF IF statement
  for (key in activeIfKeyByDepthHash) {
      if (Object.prototype.hasOwnProperty.call(activeIfKeyByDepthHash, key)) {
          activeIfKeyArray = activeIfKeyByDepthHash[key];
          if (activeIfKeyArray && key > instance.depth) {
              for (i = 0; i < activeIfKeyArray.length; i++) {
                  activeIfKeyData = activeIfKeyArray[i];

                  if (FlowDiagramBuilderApi.requireUpdateOnEndOfIfKeys(activeIfKeyData, node, instanceType)) {
                      if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF ||
                          instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE) {
                          // there must be an active starting IF node processed in the past and
                          // therefore activeIfKeyByDepthHash[instance.depth] cannot be empty. In other words, there has been IF node that inserted
                          // activeIfKeyByDepthHash[instance.depth] that has not been removed
                          activeIfKeyData.mininumDepth = instance.depth;
                          activeIfKeyByDepthHash[instance.depth].push(activeIfKeyData);
                      } else {
                          if (node.endOfIfKeys)
                              node.endOfIfKeys += ',' + activeIfKeyData.key;
                          else
                              node.endOfIfKeys = activeIfKeyData.key;
                      }
                      activeIfKeyArray.splice(i, 1);
                      i--;
                  }
              }
          }
      }
  }

  if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF) {
      if (activeIfKeyByDepthHash[instance.depth])
          activeIfKeyByDepthHash[instance.depth].push({
              key: node.key,
              mininumDepth: instance.depth,
              parallelBlockKey: node.parallelBlockKey ? node.parallelBlockKey : "",
              decisionBlockKey: node.decisionBlockKey ? node.decisionBlockKey : "",
          });
      else
          activeIfKeyByDepthHash[instance.depth] = [{
              key: node.key,
              mininumDepth: instance.depth,
              parallelBlockKey: node.parallelBlockKey ? node.parallelBlockKey : "",
              decisionBlockKey: node.decisionBlockKey ? node.decisionBlockKey : "",
          }];
  }
};

FlowDiagramBuilderApi.deleteLoopbackEdgesToInstanceParent = function(edges, instance, hierarchyStack, edgesList) {
  for (var i = 0; i < edges.length; i++) {
      if (edges[i].toPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID && edges[i].to === instance.parent) {
          // copyEdges is part of hierarchyStack so don't need to explicitly delete here
          FlowDiagramBuilderApi.removeEdgeCompletely(edges[i], hierarchyStack, edgesList);
      }
  }
};

FlowDiagramBuilderApi.processLoopback = function(instanceType, instance, node, copyEdges, newEdges, totalEdges, hierarchyStack) {
  // all of the loopback edges on higher depth that points to instance.parent should be removed and new loopback edges should be added according to type here
  for (key in FlowDiagramBuilderApi.instanceRecordsHash) {
      if (Object.prototype.hasOwnProperty.call(FlowDiagramBuilderApi.instanceRecordsHash, key)) {
          var val = FlowDiagramBuilderApi.instanceRecordsHash[key];
          if (val) {
              var edges = val.edges;
              FlowDiagramBuilderApi.deleteLoopbackEdgesToInstanceParent(edges, instance, hierarchyStack, [edges, copyEdges, newEdges, totalEdges]);
          }
      }
  }
  FlowDiagramBuilderApi.deleteLoopbackEdgesToInstanceParent(copyEdges, instance, hierarchyStack, [copyEdges, newEdges, totalEdges]);
  FlowDiagramBuilderApi.deleteLoopbackEdgesToInstanceParent(newEdges, instance, hierarchyStack, [copyEdges, newEdges, totalEdges]);

  var edgeData = {
      from: node.key,
      to: instance.parent,
      edgeId: FlowDiagramBuilderApi.edgeId,
      toPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID,
      linkLabel: FlowDiagramConstants.LINK_LABELS.loopBackConnector,
      labelColor: FlowDiagramConstants.LINK_LABEL_COLORS.loop,
      addBtnVisibility: FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopLoopback,
  };

  if (instanceType === FlowDiagramConstants.INSTANCE_TYPES.ACTION_INSTANCE_TYPE ||
      instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.FOREACH ||
      instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DOUNTIL ||
      instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PATHBROKEN ||
      instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLEL ||
      instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK ||
      instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISION ||
      instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK ||
      instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.GOBACKTO ||
      Object.keys(FlowDiagramConstants.FLOW_LOGIC_TYPES.ONE_TO_ONE).toString().includes(instanceType)) {
      // add one
      var fromPort;
      if (instanceType === FlowDiagramConstants.INSTANCE_TYPES.ACTION_INSTANCE_TYPE)
          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ACTION_OUTPUT_SYS_ID;
      else if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.FOREACH || instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DOUNTIL)
          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_OUTPUT_SYS_ID;
      else if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PATHBROKEN)
          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_PATH_BROKEN_OUTPUT_SYS_ID;
      else if (Object.keys(FlowDiagramConstants.FLOW_LOGIC_TYPES.ONE_TO_ONE).toString().includes(instanceType))
          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_ONE_OUTPUT;
      else if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLEL)
          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_MANY_OUTPUT;
      else if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK)
          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_PARALLELBLOCK_OUTPUT_SYS_ID;
      else if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISION)
          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_MANY_OUTPUT;
      else if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK)
          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_DECISIONBLOCK_OUTPUT_SYS_ID;
      else if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.GOBACKTO)
          fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_GOBACKTO_OUTPUT_SYS_ID;

      var edge = FlowDiagramTranslateModel.getEdgeModel();
      if (edge) {
          edgeData.fromPort = fromPort;

          edge = FlowDiagramBuilderApi.updateEdge(edge, edgeData);

          copyEdges.push(edge);
          newEdges.push(edge);
      }
  } else if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF) {
      // add two
      var trueEdge = FlowDiagramTranslateModel.getEdgeModel();
      if (trueEdge) {
          edgeData.fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_TRUE_SYS_ID;

          trueEdge = FlowDiagramBuilderApi.updateEdge(trueEdge, edgeData);

          copyEdges.push(trueEdge);
          newEdges.push(trueEdge);
      }

      var falseEdge = FlowDiagramTranslateModel.getEdgeModel();
      if (falseEdge) {
          edgeData.fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_FALSE_SYS_ID;

          falseEdge = FlowDiagramBuilderApi.updateEdge(falseEdge, edgeData);

          copyEdges.push(falseEdge);
          newEdges.push(falseEdge);
      }
  } else if (instanceType === FlowDiagramConstants.FLOW_LOGIC_TYPES.END) {
      // don't add
  }
};

FlowDiagramBuilderApi.processElseNode = function(errors, hierarchyStack, instance, node, totalEdges, totalNodes, newEdges) {
  var i, j;
  var addNewNode = true;
  var targetKey = node.key;
  var allPathsBlocked = false;
  if (FlowDiagramBuilderApi.instanceRecordsHash[targetKey]) {
      var targetType = FlowDiagramBuilderApi.instanceRecordsHash[targetKey].type;
      if (targetType === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE) {
          var edges = FlowDiagramBuilderApi.instanceRecordsHash[targetKey].edges;
          var trueEdge;
          var falseEdge;
          for (i = 0; i < edges.length; i++) {
              if (edges[i].fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_FALSE_SYS_ID && edges[i].toPort !== FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID)
                  falseEdge = edges[i];
              else if (edges[i].fromPort === FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_TRUE_SYS_ID && edges[i].toPort !== FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID)
                  trueEdge = edges[i];
          }

          // newTargetKey is node's key next to ELSE node and all edges pointing to the ELSE node should point to newTargetKey
          var newTargetNodeKey = "";
          for (key in FlowDiagramBuilderApi.instanceRecordsHash) {
              if (Object.prototype.hasOwnProperty.call(FlowDiagramBuilderApi.instanceRecordsHash, key)) {
                  var val = FlowDiagramBuilderApi.instanceRecordsHash[key];
                  if (val && val.edges) {
                      for (i = 0; i < val.edges.length; i++) {
                          // Find the TRUE edge that is pointing (edge.to === node.key) to ELSE node. Make the found edge point to where ELSE node is pointing to.
                          if (val.edges[i].to === targetKey && trueEdge) {
                              newTargetNodeKey = trueEdge.to; // trueEdge.to usually points to END node at this point
                              val.edges[i].to = trueEdge.to;
                              val.edges[i].toPort = trueEdge.toPort;
                              if (hierarchyStack[hierarchyStack.length - 1] && hierarchyStack[hierarchyStack.length - 1].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE) {
                                  hierarchyStack[hierarchyStack.length - 1].edges.push(val.edges[i]);
                              }
                          }
                      }
                  }
              }
          }

          if (falseEdge)
              FlowDiagramBuilderApi.removeEdgeCompletely(falseEdge, hierarchyStack, [edges, newEdges, totalEdges]);
          if (trueEdge)
              FlowDiagramBuilderApi.removeEdgeCompletely(trueEdge, hierarchyStack, [edges, newEdges, totalEdges]);

          // don't add the ELSE node to totalNodes/diagramJSON
          addNewNode = false;

          // add entry in the hash to identify which node replaces the else node deleted
          FlowDiagramBuilderApi.addElseReplacementHash(node, newTargetNodeKey);
      }
  }
  return addNewNode;
};

FlowDiagramBuilderApi.addElseReplacementHash = function(node, newTargetNodeKey) {
  if (node && FlowDiagramBuilderApi.elseReplacementHash) {
      FlowDiagramBuilderApi.elseReplacementHash[node.key] = {
          key: FlowDiagramBuilderApi.previousIfOrElseIfKey,
          newTargetNodeKey: newTargetNodeKey,
          node: node
      };
  }
};

// previousKeys are keys previously visited by this recursion. used to prevent stack overflow. Stored in resultArray[1]
// highestOrder (resultArray[0]) is used to find out the highest order of node visited.
// if end node is visited then highest order = FlowDiagramBuilderApi.instanceRecordsOrderedList.length
// if endNodeKey is reached then that means there is no path blockage up to endNodeKey and therefore return false
FlowDiagramBuilderApi.isAllEdgesEndWithoutReachingTheEnd = function(key, previousKeys, totalEdges, resultArray, endNodeKey) {
  // there could be an edge that has "from key" = "to key" (example: loopback edge to itself) so just return true in below
  // below checks for if it is processing the same key that has been processed before
  if (key && previousKeys.includes(key))
      return true;
  else
      previousKeys += (key + ",");

  if (key === FlowDiagramBuilderApi.endKey) {
      resultArray[0] = FlowDiagramBuilderApi.instanceRecordsOrderedList.length;
      resultArray[1] = previousKeys;
      return false;
  } else if (key === endNodeKey && FlowDiagramBuilderApi.instanceRecordsHash[key]) {
      resultArray[0] = FlowDiagramBuilderApi.instanceRecordsHash[key].customOrder;
      resultArray[1] = previousKeys;
      return false;
  } else if (FlowDiagramBuilderApi.instanceRecordsHash[key] && (FlowDiagramBuilderApi.instanceRecordsHash[key].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.END || FlowDiagramBuilderApi.instanceRecordsHash[key].type === FlowDiagramConstants.FLOW_LOGIC_TYPES.GOBACKTO)) {
      if (FlowDiagramBuilderApi.instanceRecordsHash[key].customOrder > resultArray[0]) {
          resultArray[0] = FlowDiagramBuilderApi.instanceRecordsHash[key].customOrder;
          resultArray[1] = previousKeys;
      }
      return true;
  }
  for (var i = 0; i < totalEdges.length; i++) {
      // loopbacks don't count since it will never be a leaf node
      if (totalEdges[i] && totalEdges[i].from && totalEdges[i].to && totalEdges[i].from === key && totalEdges[i].toPort !== FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID) {
          if (FlowDiagramBuilderApi.isAllEdgesEndWithoutReachingTheEnd(totalEdges[i].to, previousKeys, totalEdges, resultArray, endNodeKey) === false)
              return false;
      }
  }
  return true;
};

FlowDiagramBuilderApi.insertInstanceRecordInDepthArray = function(record) {
  var depthArray;
  if (!record.depth) {
      if (record.parent) {
          var recordParent = FlowDiagramBuilderApi.instanceRecordsHash[record.parent];
          if (recordParent)
              record.depth = 1 + recordParent.depth;
          else
              record.depth = 0;
      } else {
          record.depth = 1;
      }
  }
  depthArray = FlowDiagramBuilderApi.getInstanceRecordInDepthArray(record.depth);
  depthArray.push(record);
};

FlowDiagramBuilderApi.getInstanceRecordInDepthArray = function(depth) {
  if (!FlowDiagramBuilderApi.instanceRecordsDepthHash[depth])
      FlowDiagramBuilderApi.instanceRecordsDepthHash[depth] = [];
  return FlowDiagramBuilderApi.instanceRecordsDepthHash[depth];
};

FlowDiagramBuilderApi.addNewEdges = function(instance, node, newEdges, copyEdges, hierarchyStack, totalNodes) {
  var newEdge;
  var instanceRecordsHashEdges = [];
  var i, j;
  var type = "";
  for (i = 0; i < copyEdges.length; i++)
      instanceRecordsHashEdges.push(copyEdges[i]);

  if (instance.actionType || instance.subFlow) {
      type = FlowDiagramConstants.INSTANCE_TYPES.ACTION_INSTANCE_TYPE;
      FlowDiagramBuilderApi.instanceRecordsHash[instance.uiUniqueIdentifier] = {
          key: instance.uiUniqueIdentifier,
          parent: instance.parent,
          type: type,
          edges: instanceRecordsHashEdges,
          deletedLoopbackEdges: null,
          connectedCount: 0,
          depth: instance.depth,
      };
      FlowDiagramBuilderApi.insertInstanceRecordInDepthArray(FlowDiagramBuilderApi.instanceRecordsHash[instance.uiUniqueIdentifier]);
      FlowDiagramBuilderApi.insertInstanceRecordInOrderedListWithOrder(FlowDiagramBuilderApi.instanceRecordsHash[instance.uiUniqueIdentifier]);

      newEdge = FlowDiagramTranslateModel.getEdgeModel();
      if (newEdge) {
          newEdge = FlowDiagramBuilderApi.updateEdge(
              newEdge, {
                  from: node.key,
                  to: FlowDiagramBuilderApi.endKey,
                  fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ACTION_OUTPUT_SYS_ID,
                  toPort: FlowDiagramBuilderApi.endPort,
                  edgeId: FlowDiagramBuilderApi.edgeId,
              }
          );

          newEdges.push(newEdge);
          copyEdges.push(newEdge);
          instanceRecordsHashEdges.push(newEdge);
      }
  } else if (instance.flowLogicDefinition && instance.flowLogicDefinition.type) {
      type = instance.flowLogicDefinition.type;
      FlowDiagramBuilderApi.instanceRecordsHash[instance.uiUniqueIdentifier] = {
          key: instance.uiUniqueIdentifier,
          parent: instance.parent,
          orderBottom: instance.orderBottom,
          data: instance.data,
          type: type,
          edges: instanceRecordsHashEdges,
          deletedLoopbackEdges: null,
          connectedCount: 0,
          depth: instance.depth,
      };
      FlowDiagramBuilderApi.insertInstanceRecordInDepthArray(FlowDiagramBuilderApi.instanceRecordsHash[instance.uiUniqueIdentifier]);
      FlowDiagramBuilderApi.insertInstanceRecordInOrderedListWithOrder(FlowDiagramBuilderApi.instanceRecordsHash[instance.uiUniqueIdentifier]);

      if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.IF || instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSE || instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.ELSEIF) {
          var updatedEdgeData = {
              from: node.key,
              to: FlowDiagramBuilderApi.endKey,
              toPort: FlowDiagramBuilderApi.endPort,
              edgeId: FlowDiagramBuilderApi.edgeId,
              labelColor: FlowDiagramConstants.LINK_LABEL_COLORS.decision,
          };

          var newEdgeTrue = FlowDiagramTranslateModel.getEdgeModel();
          if (newEdgeTrue) {
              updatedEdgeData.fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_TRUE_SYS_ID;
              updatedEdgeData.linkLabel = FlowDiagramConstants.LINK_LABELS.trueConnector;

              newEdgeTrue = FlowDiagramBuilderApi.updateEdge(newEdgeTrue, updatedEdgeData);

              newEdges.push(newEdgeTrue);
              copyEdges.push(newEdgeTrue);
              instanceRecordsHashEdges.push(newEdgeTrue);
          }

          var newEdgeFalse = FlowDiagramTranslateModel.getEdgeModel();
          if (newEdgeFalse) {
              updatedEdgeData.fromPort = FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_IF_ELSE_OUTPUT_FALSE_SYS_ID;
              updatedEdgeData.linkLabel = FlowDiagramConstants.LINK_LABELS.falseConnector;

              newEdgeFalse = FlowDiagramBuilderApi.updateEdge(newEdgeFalse, updatedEdgeData);

              newEdges.push(newEdgeFalse);
              copyEdges.push(newEdgeFalse);
              instanceRecordsHashEdges.push(newEdgeFalse);
          }
      } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.FOREACH || instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DOUNTIL) {

          var newEdgeLoopContinue = FlowDiagramTranslateModel.getEdgeModel();
          if (newEdgeLoopContinue) {
              newEdgeLoopContinue = FlowDiagramBuilderApi.updateEdge(
                  newEdgeLoopContinue, {
                      from: node.key,
                      to: FlowDiagramBuilderApi.endKey,
                      fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_OUTPUT_SYS_ID,
                      toPort: FlowDiagramBuilderApi.endPort,
                      edgeId: FlowDiagramBuilderApi.edgeId,
                      linkLabel: "", // intentionally left blank
                      labelColor: FlowDiagramConstants.LINK_LABEL_COLORS.loop,
                      addBtnVisibility: FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopWithChildEnd,
                  }
              );
              newEdges.push(newEdgeLoopContinue);
              copyEdges.push(newEdgeLoopContinue);
              instanceRecordsHashEdges.push(newEdgeLoopContinue);
          }

          var newEdgeLoopBack = FlowDiagramTranslateModel.getEdgeModel();
          if (newEdgeLoopBack) {
              newEdgeLoopBack = FlowDiagramBuilderApi.updateEdge(
                  newEdgeLoopBack, {
                      from: node.key,
                      to: node.key,
                      fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_OUTPUT_SYS_ID,
                      toPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_FOR_INPUT_LOOP_BACK_SYS_ID,
                      edgeId: FlowDiagramBuilderApi.edgeId,
                      linkLabel: "", // intentionally left blank
                      labelColor: FlowDiagramConstants.LINK_LABEL_COLORS.loop,
                      addBtnVisibility: FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopLoopback,
                  }
              );

              newEdges.push(newEdgeLoopBack);
              copyEdges.push(newEdgeLoopBack);
              instanceRecordsHashEdges.push(newEdgeLoopBack);
          }
      } else if (Object.keys(FlowDiagramConstants.FLOW_LOGIC_TYPES.ONE_TO_ONE).toString().includes(instance.flowLogicDefinition.type)) {
          var newLogicEdge = FlowDiagramTranslateModel.getEdgeModel();
          if (newLogicEdge) {
              var updatedLogicEdgeData = {
                  from: node.key,
                  to: FlowDiagramBuilderApi.endKey,
                  toPort: FlowDiagramBuilderApi.endPort,
                  fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_ONE_OUTPUT,
                  edgeId: FlowDiagramBuilderApi.edgeId
              };

              newLogicEdge = FlowDiagramBuilderApi.updateEdge(newLogicEdge, updatedLogicEdgeData);

              newEdges.push(newLogicEdge);
              copyEdges.push(newLogicEdge);
              instanceRecordsHashEdges.push(newLogicEdge);
          }
      } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.END) {
          // Intentionally empty for node.node_id === FlowDiagramConstants.SYS_IDS.FLOW_LOGIC_END_ACTION; END node does not have an output edge
      } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLEL) {
          var newEdgeParallel = FlowDiagramTranslateModel.getEdgeModel();
          if (newEdgeParallel) {
              if (!instance.children || instance.children.length === 0) {
                  newEdgeParallel = FlowDiagramBuilderApi.updateEdge(
                      newEdgeParallel, {
                          from: node.key,
                          to: FlowDiagramBuilderApi.endKey,
                          fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_MANY_OUTPUT,
                          toPort: FlowDiagramConstants.DIAGRAM_PORT_END_PARALLEL_OUTPUT_SYS_ID,
                          edgeId: FlowDiagramBuilderApi.edgeId,
                          linkLabel: "", // intentionally left blank
                          labelColor: FlowDiagramConstants.LINK_LABEL_COLORS.decision,
                          addBtnVisibility: FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopEmptyEnd,
                          addBtnText: "+ Add branch",
                      }
                  );
              } else {
                  newEdgeParallel = FlowDiagramBuilderApi.updateEdge(
                      newEdgeParallel, {
                          from: node.key,
                          to: node.key,
                          fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_ONE_OUTPUT,
                          toPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_MANY_INPUT,
                          edgeId: FlowDiagramBuilderApi.edgeId,
                          linkLabel: "", // intentionally left blank
                          labelColor: FlowDiagramConstants.LINK_LABEL_COLORS.decision,
                          addBtnVisibility: FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopLoopback,
                      }
                  );
                  FlowDiagramBuilderApi.parallelBranchAddLookup[node.key] = instance.children[instance.children.length - 1]; // capture key of last child for turning on add branch on
              }
              newEdges.push(newEdgeParallel);
              copyEdges.push(newEdgeParallel);
              instanceRecordsHashEdges.push(newEdgeParallel);
          }
      } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.PARALLELBLOCK) {
          // add extra edge to connect between PARALLEL and PARALLELBLOCK (current node)
          // PARALLEL will always be the instance.parent of PARALLELBLOCK
          var newEdgeFromParallel = FlowDiagramTranslateModel.getEdgeModel();
          if (newEdgeFromParallel) {
              if (FlowDiagramBuilderApi.parallelBranchAddLookup[instance.parent] && FlowDiagramBuilderApi.parallelBranchAddLookup[instance.parent] === node.key) {
                  newEdgeFromParallel = FlowDiagramBuilderApi.updateEdge(
                      newEdgeFromParallel, {
                          from: instance.parent,
                          to: node.key,
                          fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_MANY_OUTPUT,
                          toPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_PARALLELBLOCK_INPUT_SYS_ID,
                          edgeId: FlowDiagramBuilderApi.edgeId,
                          linkLabel: "", // intentionally left blank
                          labelColor: FlowDiagramConstants.LINK_LABEL_COLORS.decision,
                          addBtnVisibility: FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopEmptyEnd,
                          addBtnText: "+ Add branch"
                      }
                  );
              } else {
                  newEdgeFromParallel = FlowDiagramBuilderApi.updateEdge(
                      newEdgeFromParallel, {
                          from: instance.parent,
                          to: node.key,
                          fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_MANY_OUTPUT,
                          toPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_PARALLELBLOCK_INPUT_SYS_ID,
                          edgeId: FlowDiagramBuilderApi.edgeId,
                          linkLabel: "", // intentionally left blank
                          labelColor: FlowDiagramConstants.LINK_LABEL_COLORS.decision,
                          addBtnVisibility: FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopLoopback
                      }
                  );
              }
              newEdges.push(newEdgeFromParallel);
              copyEdges.push(newEdgeFromParallel);
              instanceRecordsHashEdges.push(newEdgeFromParallel);
          }

          var newEdgeParallelBlock = FlowDiagramTranslateModel.getEdgeModel();
          if (newEdgeParallelBlock) {
              newEdgeParallelBlock = FlowDiagramBuilderApi.updateEdge(
                  newEdgeParallelBlock, {
                      from: node.key,
                      to: FlowDiagramBuilderApi.endKey,
                      fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_PARALLELBLOCK_OUTPUT_SYS_ID,
                      toPort: FlowDiagramBuilderApi.endPort,
                      edgeId: FlowDiagramBuilderApi.edgeId,
                      linkLabel: "", // intentionally left blank
                      labelColor: FlowDiagramConstants.LINK_LABEL_COLORS.decision,
                      addBtnVisibility: FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopEmptyEnd,
                  }
              );
              newEdges.push(newEdgeParallelBlock);
              copyEdges.push(newEdgeParallelBlock);
              instanceRecordsHashEdges.push(newEdgeParallelBlock);
          }
      } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISION) {
          var newEdgeDecision = FlowDiagramTranslateModel.getEdgeModel();
          if (newEdgeDecision) {
              if (!instance.children || instance.children.length === 0) {
                  // If there is no children, then there is no associated DECISIONBLOCK. Currently we add the edge between DECISION
                  // and DECISIONBLOCK when DECISIONBLOCK is added and therefore we need to add an edge to END node if there is no
                  // associated DECISIONBLOCK
                  newEdgeDecision = FlowDiagramBuilderApi.updateEdge(
                      newEdgeDecision, {
                          from: node.key,
                          to: FlowDiagramBuilderApi.endKey,
                          fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_MANY_OUTPUT,
                          toPort: FlowDiagramBuilderApi.endPort,
                          edgeId: FlowDiagramBuilderApi.edgeId,
                          linkLabel: "", // intentionally left blank
                          labelColor: FlowDiagramConstants.LINK_LABEL_COLORS.decision,
                          addBtnVisibility: FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopLoopback,
                      }
                  );
                  newEdges.push(newEdgeDecision);
                  copyEdges.push(newEdgeDecision);
                  instanceRecordsHashEdges.push(newEdgeDecision);
              }

              if (instance.decisionTableInformation && instance.decisionTableInformation.decision_answers instanceof Array) {
                  var decisionAnswers = instance.decisionTableInformation.decision_answers;
                  for (j = 0; j < decisionAnswers.length; j++) {
                      if (decisionAnswers[j].valid && decisionAnswers[j].value) {
                          // FlowDiagramBuilderApi.decisionAnswers[decisionAnswers[j].value] can be overwritten with same value of decisionAnswers[j].label
                          // and that is fine
                          FlowDiagramBuilderApi.decisionAnswers[decisionAnswers[j].value] = decisionAnswers[j].label;
                      }
                  }
              }
          }
          // Update node display name based on label
          if (node && node.data && node.data.decision_label) {
              node.name = node.data.decision_label;
          } else {
              node.name = FlowDiagramConstants.NODE_NAMES.DECISION;
          }
      } else if (instance.flowLogicDefinition.type === FlowDiagramConstants.FLOW_LOGIC_TYPES.DECISIONBLOCK) {
          // add extra edge to connect between DECISION and DECISIONBLOCK (current node)
          // DECISION will always be the instance.parent of DECISIONBLOCK
          var newEdgeFromDecision = FlowDiagramTranslateModel.getEdgeModel();
          if (newEdgeFromDecision) {
              newEdgeFromDecision = FlowDiagramBuilderApi.updateEdge(
                  newEdgeFromDecision, {
                      from: instance.parent,
                      to: node.key,
                      fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_ONE_TO_MANY_OUTPUT,
                      toPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_DECISIONBLOCK_INPUT_SYS_ID,
                      edgeId: FlowDiagramBuilderApi.edgeId,
                      linkLabel: "", // intentionally left blank
                      labelColor: FlowDiagramConstants.LINK_LABEL_COLORS.decision,
                      addBtnVisibility: FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopEmptyEnd
                  }
              );
              newEdges.push(newEdgeFromDecision);
              copyEdges.push(newEdgeFromDecision);
              instanceRecordsHashEdges.push(newEdgeFromDecision);
          }

          var newEdgeDecisionBlock = FlowDiagramTranslateModel.getEdgeModel();
          if (newEdgeDecisionBlock) {
              newEdgeDecisionBlock = FlowDiagramBuilderApi.updateEdge(
                  newEdgeDecisionBlock, {
                      from: node.key,
                      to: FlowDiagramBuilderApi.endKey,
                      fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_DECISIONBLOCK_OUTPUT_SYS_ID,
                      toPort: FlowDiagramBuilderApi.endPort,
                      edgeId: FlowDiagramBuilderApi.edgeId,
                      linkLabel: "", // intentionally left blank
                      labelColor: FlowDiagramConstants.LINK_LABEL_COLORS.decision,
                      addBtnVisibility: FlowDiagramConstants.LINK_ADD_BTN_VISIBILITY.forLoopEmptyEnd,
                  }
              );
              newEdges.push(newEdgeDecisionBlock);
              copyEdges.push(newEdgeDecisionBlock);
              instanceRecordsHashEdges.push(newEdgeDecisionBlock);
          }

          if (instance.inputs instanceof Array) {
              // Finding appropriate name/label for this decision block
              var newName;
              for (j = 0; j < instance.inputs.length; j++) {
                  var instanceInput = instance.inputs[j];

                  // If this is the label input and it is populated, use that
                  if (instanceInput.field_name === 'label' && instanceInput.value) {
                      newName = instanceInput.value;
                  }
                  // Otherwise, if we haven't found the name already, use the default decision answer label
                  else if (!newName && instanceInput.field_name === 'answer') {
                      if (instanceInput.value === '')
                          newName = 'Otherwise';
                      else if (instanceInput.value) {
                          var label = FlowDiagramBuilderApi.decisionAnswers[instanceInput.value];
                          if (label)
                              newName = label;
                      }
                  }
              }
              if (newName) {
                  node.name = newName;
              }
          }
      } else if (type === FlowDiagramConstants.FLOW_LOGIC_TYPES.GOBACKTO) {
          var newEdge = FlowDiagramTranslateModel.getEdgeModel();
          if (newEdge) {
              // to, toPort, linkLabel will be updated later
              var to = FlowDiagramBuilderApi.endKey;
              var toPort = FlowDiagramBuilderApi.endPort;
              newEdge = FlowDiagramBuilderApi.updateEdge(
                  newEdge, {
                      from: node.key,
                      to: to,
                      fromPort: FlowDiagramConstants.SYS_IDS.DIAGRAM_PORT_GOBACKTO_OUTPUT_SYS_ID,
                      toPort: toPort,
                      edgeId: FlowDiagramBuilderApi.edgeId,
                      addBtnVisibility: "never",
                      linkLabel: "",
                      labelColor: FlowDiagramConstants.LINK_LABEL_COLORS.loop,
                  }
              );
              newEdges.push(newEdge);
              copyEdges.push(newEdge);
          }
      }
  }
  // need to separate copyEdges vs newEdges because an edge can be taken out from copyEdges array whereas
  //  no edge will be taken out from newEdges
  hierarchyStack.push({
      key: instance.uiUniqueIdentifier,
      parent: instance.parent,
      depth: instance.depth,
      edges: copyEdges,
      type: type,
      deletedLoopbackEdges: null,
  });
  FlowDiagramBuilderApi.recordOrder++;
};

FlowDiagramBuilderApi.insertInstanceRecordInOrderedListWithOrder = function(record) {
  record.customOrder = FlowDiagramBuilderApi.recordOrder;
  FlowDiagramBuilderApi.instanceRecordsOrderedList.push(record);
};

// assigns "type" to flow_diagram_type field for all records in an array
FlowDiagramBuilderApi.assignFlowDiagramType = function(targetArray, type) {
  if (targetArray instanceof Array) {
      for (var i = 0; i < targetArray.length; i++)
          targetArray[i].flow_diagram_type = type;
  }
};

FlowDiagramBuilderApi.updateEdge = function(edge, updateData) {
  if (edge) {
      edge.from = updateData.from;
      edge.to = updateData.to;
      edge.fromPort = updateData.fromPort || updateData.fromPort;
      edge.toPort = updateData.toPort || updateData.toPort;
      edge.edge_id = updateData.edgeId;
      edge.linkLabel = updateData.linkLabel || '';
      edge.labelColor = updateData.labelColor || '';
      edge.addBtnVisibility = updateData.addBtnVisibility || 'onHover';
      edge.addBtnText = updateData.addBtnText || '';
      edge.hideToArrow = updateData.hideToArrow || false;
  }
  return edge;
};

FlowDiagramBuilderApi.getDiagramDifference = function(newDiagram, oldDiagram, errors) {
  if (!errors || !(errors instanceof Array))
      errors = [];

  function arrayToMap(arr, keyFunc) {
      // Map is not defined in rhino engine
      var result = {};
      arr.forEach(function(item) {
          var key = keyFunc(item);
          result[key] = item;
      });
      return result;
  }


  function getNodeKey(node) {
      return node.key;
  }

  function getEdgeKey(edge) {
      return edge.from + "|" + edge.to + "|" + edge.fromPort + "|" + edge.toPort;
  }

  function getAddedItems(newArr, oldMap, keyFunc) {
      return newArr.filter(function(item) {
          return !oldMap.hasOwnProperty(keyFunc(item));
      });
  }

  function getDeletedItems(oldArr, newMap, keyFunc) {
      return oldArr.filter(function(item) {
          return !newMap.hasOwnProperty(keyFunc(item));
      });
  }

  function isEquivalentValue(v1, v2) {
      if (Array.isArray(v1) && Array.isArray(v2)) {
          return isEquivalentArray(v1, v2);
      } else if (
          v1 !== null &&
          typeof v1 === "object" &&
          v2 !== null &&
          typeof v2 === "object"
      ) {
          // the typeof an Array value is also "object"
          return isEquivalentObject(v1, v2);
      } else {
          return v1 == v2;
      }
  }

  function isEquivalentArray(arr1, arr2) {
      if (arr1.length !== arr2.length) {
          return false;
      }

      for (var i = 0; i < arr1.length; i++) {
          if (!isEquivalentValue(arr1[i], arr2[i])) {
              return false;
          }
      }

      return true;
  }

  function isEquivalentObject(obj1, obj2, propNames) {
      var props1 = Object.getOwnPropertyNames(obj1);
      var props2 = Object.getOwnPropertyNames(obj2);

      if (propNames !== undefined) {
          props1 = props1.filter(function(name) {
              return propNames.indexOf(name) !== -1;
          });
          props2 = props2.filter(function(name) {
              return propNames.indexOf(name) !== -1;
          });
      }

      if (props1.length !== props2.length) {
          return false;
      }

      for (var i = 0; i < props1.length; i++) {
          var propName = props1[i];
          if (!isEquivalentValue(obj1[propName], obj2[propName])) {
              return false;
          }
      }

      return true;
  }

  function getUpdatedItems(newArray, oldMap, keyFunc, propNames) {
      return newArray.filter(function(item) {
          var oldItem = oldMap[keyFunc(item)];
          if (!oldItem) {
              return false;
          }

          return !isEquivalentObject(item, oldItem, propNames);
      });
  }

  var newNodes = newDiagram.diagramJSON.nodes;
  var oldNodes = oldDiagram.diagramJSON.nodes;

  var newEdges = newDiagram.diagramJSON.edges;
  var oldEdges = oldDiagram.diagramJSON.edges;

  // key -> node object
  var newNodeMap = arrayToMap(newNodes, getNodeKey);
  var oldNodeMap = arrayToMap(oldNodes, getNodeKey);

  // from|to|fromPort|toPort -> edge object
  var newEdgeMap = arrayToMap(newEdges, getEdgeKey);
  var oldEdgeMap = arrayToMap(oldEdges, getEdgeKey);

  var addedNodes = getAddedItems(newNodes, oldNodeMap, getNodeKey);
  // make sure that we do not compare the template related attributes
  var updatedNodes = getUpdatedItems(newNodes, oldNodeMap, getNodeKey, ['node_id', 'name', 'instance_sys_id', 'definition_sys_id', 'instanceType', 'order', 'relation', 'data']);
  var deletedNodes = getDeletedItems(oldNodes, newNodeMap, getNodeKey);

  var addedEdges = getAddedItems(newEdges, oldEdgeMap, getEdgeKey);
  var deletedEdges = getDeletedItems(oldEdges, newEdgeMap, getEdgeKey);
  var updatedEdges = getUpdatedItems(newEdges, oldEdgeMap, getEdgeKey, ['linkLabel']);

  var diff = {
      nodes: {
          added: addedNodes,
          updated: updatedNodes,
          deleted: deletedNodes
      },
      edges: {
          added: addedEdges,
          deleted: deletedEdges,
          updated: updatedEdges
      },
  };

  return diff;
};

Sys ID

60e7cec1ff3030104ef14ee9453bf1eb

Offical Documentation

Official Docs: