var getItemLocalStorage = global.getItemLocalStorage;
var setItemLocalStorage = global.setItemLocalStorage;
var p5WFS_GetFeature = global.p5WFS_GetFeature;
var renderGraph = global.renderGraph; // @from sankey-init
var DBG = DBG || 0;
var DBG1 = 1;
var globalGraphStore = null;
function graphStore(state, action) {
	var prevState = state || {
		isLoading: false,
		sentRequestId: 0,
		receivedRequestId: 0,
		items: [],
	};
	DBG && console.log('DBG: graph store', { prevState, action });
	switch (action.type) {
		case 'SET_SENT_REQUEST_ID': return Object.assign(prevState, {
			sentRequestId: action.sentRequestId,
			isLoading: true
		});
		case 'SET_RESPONSE': return Object.assign(prevState, {
			receivedRequestId: action.requestId,
			items: action.items,
			isLoading: (prevState.sentRequestId > action.requestId) ? true : false,
		});
		default: return prevState;
	}
}
var createGraphActions = function () {

	var setSentRequestId = function (sentRequestId) {
		return { type: 'SET_SENT_REQUEST_ID', sentRequestId: sentRequestId };
	}
	var setResponse = function (obj) {
		return Object.assign(obj, { type: 'SET_RESPONSE' });
	}

	var fetchData = function (nameSection) {
		return function (dispatch, getState) {
			var state = getState();
			var this__requestId = state.sentRequestId + 1;
			dispatch(setSentRequestId(this__requestId));

			var reqPromise = graphFetchData(nameSection);
			return reqPromise.then(function (response) {
				var items = response;
				var state = getState();

				if (state.receivedRequestId > this__requestId) {
					DBG && console.log('DBG: skiped response', { 'state.receivedRequestId': state.receivedRequestId, this__requestId });
					return;
				}

				dispatch(
					setResponse({
						requestId: this__requestId,
						msg: "Pobrano dane",
						typeName: ('pracownicy' === nameSection) ? 'default_db__x3A__BI_audit_ENERGA_PRACOWNICY:BI_audit_ENERGA_PRACOWNICY' : 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI:BI_audit_ENERGA_RUM_KONTRAHENCI',
						items: items
					})
				);
			}).catch(function (e) {
				p5UI__notifyAjaxCallback({type: 'error', msg: 'Wystąpił błąd #GS1: ' + e});
				// dispatch( setErrorMsg(e) ); // TODO: show error with msg and refresh button
			});
		}
	}

	return {
		fetchData: fetchData,
	}
}
var globalGraphActions = ''; // createGraphActions();

var createReactClass = global.p5VendorJs.createReactClass;
var h = global.p5VendorJs.React.createElement;
var ReactDOM = global.p5VendorJs.ReactDOM;
var Redux = global.p5VendorJs.Redux;
var ReduxThunk = global.p5VendorJs.ReduxThunk;
var createStoreWithThunkMiddleware = Redux.applyMiddleware(ReduxThunk)(Redux.createStore); // TODO: to vendor.js

global['P5UI__BocianGraphView'] = createReactClass({
	// props.nameSection: PropTypes.string.isRequired
	// props.store: Redux store with state: { isLoading: false, sentRequestId: 0, receivedRequestId: 0, items: [] }
	// props.actions: Redux store actions {  }
	getStateFromStore: function () {
		var state = this.props.store.getState();
		return {
			isLoading: state.isLoading,
			items: state.items,
			sentRequestId: state.sentRequestId,
			receivedRequestId: state.receivedRequestId,
		};
	},
	getInitialState: function () {
		return this.getStateFromStore();
	},
	componentDidMount: function () {
		DBG && console.log('DBG::P5UI__BocianGraphView::componentDidMount');
		this.unsubscribe = this.props.store.subscribe(this.storeUpdated)
	},
	componentWillUnmount: function () {
		this.unsubscribe()
	},
	storeUpdated: function () {
		DBG && console.log('DBG::P5UI__BocianGraphView::storeUpdated');
		this.setState(this.getStateFromStore())
	},
	shouldComponentUpdate: function (nextProps, nextState) {
		DBG && console.log('DBG::P5UI__BocianGraphView::shouldComponentUpdate', { state: this.state, nextState});
		// return (
		// 	this.state.isLoading !== nextState.isLoading
		// 	|| this.state.sentRequestId !== nextState.sentRequestId
		// 	|| this.state.receivedRequestId !== nextState.receivedRequestId
		// );
		if (nextState.receivedRequestId > this.state.receivedRequestId) {
			var htmlId = 'p5_graphView_' + this.props.nameSection; // #smad-'+nameSection+'-graph-view
			graphRaportRender({
				msg: "Pobrano dane",
				typeName: ('pracownicy' === this.props.nameSection) ? 'default_db__x3A__BI_audit_ENERGA_PRACOWNICY:BI_audit_ENERGA_PRACOWNICY' : 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI:BI_audit_ENERGA_RUM_KONTRAHENCI',
				items: nextState.items
			}, document.getElementById(htmlId))
		}
		return (
			this.state.isLoading !== nextState.isLoading
		);
	},
	render: function () {
		DBG && console.log('DBG::P5UI__BocianGraphView::render', { props: this.props, state: this.state });
		var htmlId = 'p5_graphView_' + this.props.nameSection; // #smad-'+nameSection+'-graph-view
		var msg = (this.state.isLoading) ? "Pobieranie danych..." : null;
		return h('div', {}, [
			msg && h('div', {
				className: 'alert alert-info',
				style: {
					maxWidth: '600px',
					margin: '0 auto',
					padding: '3px 24px',
				}
			}, msg),
			h('div', { id: htmlId })
		]);
	}
});

var btnRefresh = null;
var btnHide = null;
function graphShowHide(btnNode, nameSection) {
	var node = $('#smad-'+nameSection+'-graph-view')
	if (!node || !node.length) {
		console.log("Missing dom node '#smad-"+nameSection+"-graph-view'")
		return;
	}
	var btnJqNode = jQuery(btnNode);
	if ('block' == node.css('display')) {
		DBG && console.warn("DBG:graphShowHide: hide");
		node.hide()
		btnRefresh.remove();
		btnHide.remove();
		return;
	}
	node.show()
	btnRefresh = jQuery('<button class="btn btn-primary" style="padding:1px 5px">odśwież</button>').appendTo(btnJqNode.parent());
	btnRefresh.on('click', function () {
		globalGraphStore.dispatch(globalGraphActions.fetchData(nameSection));
	})
	btnHide = jQuery('<button class="btn btn-primary" style="padding:1px 5px"><i class="glyphicon glyphicon-remove"></i></button>').appendTo(btnJqNode.parent());
	btnHide.on('click', function () {
		node.hide()
		btnRefresh.remove();
		btnHide.remove();
	})
	var graphResultNode = node.get(0)

	if (!globalGraphStore) {
		globalGraphStore = createStoreWithThunkMiddleware(graphStore);
		globalGraphActions = createGraphActions();
		ReactDOM.render(
			h(P5UI__BocianGraphView, {
				nameSection: nameSection,
				store: globalGraphStore,
				actions: globalGraphActions,
			}),
			graphResultNode
		);
	}

	globalGraphStore.dispatch(globalGraphActions.fetchData(nameSection));
}

function graphFetchData(nameSection) { // @return Promise
	var page = page || getItemLocalStorage('Bocian.biAuditForm.'+nameSection+'.pagination.page');
	if ( page === 1) {
		setItemLocalStorage('Bocian.biAuditForm.'+nameSection+'.pagination.page', 1);
	}

	var filterIdGroup = getItemLocalStorage('Bocian.biAuditForm.'+nameSection+'.filterIdGroup');
	var filterIdRaport = getItemLocalStorage('Bocian.biAuditForm.'+nameSection+'.filterIdRaport');

	var frm = document.getElementById('filtersFieldRemoveBtn-' + nameSection.toUpperCase()).form
	var fieldNameList = ('pracownicy' === nameSection) ? FIELD_LIST_PRACOWNICY : FIELD_LIST_KONTRAHENCI
	var filterFields = fieldNameList.filter(function (fieldName) {
		if (!frm[fieldName] && DBG) console.log('Err missing field: "'+fieldName+'"')
		return (frm[fieldName]) ? true : false
	}).map(function (fieldName) {
		return [ fieldName, frm[fieldName].value ]
	}).filter(function (filter) {
		return ( filter[1].length > 0 )
	})
	// filterFields = (filterFields.length > 0) ? '&' + filterFields : ''

	// <ogc:Filter>
	//     <ogc:Or>
	//       <ogc:PropertyIsLike wildCard="*" singleChar="%23" escapeChar="!">
	//         <ogc:PropertyName>A_STATUS</ogc:PropertyName>
	//         <ogc:Literal>*O%23MA*</ogc:Literal>
	//       </ogc:PropertyIsLike>
	//       <ogc:PropertyIsLike wildCard="*" singleChar="%23" escapeChar="!">
	//         <ogc:PropertyName>A_STATUS</ogc:PropertyName>
	//         <ogc:Literal>*ARNING</ogc:Literal>
	//       </ogc:PropertyIsLike>
	//     </ogc:Or>
	//   </ogc:Filter>
	var ogcFilterFields = (filterFields.length > 0)
	? '<ogc:Filter><ogc:And>' + filterFields.map(function(filter) {
		if ('ID' === filter[0].substr(2)) return '<ogc:PropertyIsEqualTo>' +
			'<ogc:PropertyName>' + filter[0].substr(2) + '</ogc:PropertyName>' +
			'<ogc:Literal>' + filter[1] + '</ogc:Literal>' +
		'</ogc:PropertyIsEqualTo>';

		return '<ogc:PropertyIsLike wildCard="*" singleChar="%23" escapeChar="!">' +
			'<ogc:PropertyName>' + filter[0].substr(2) + '</ogc:PropertyName>' +
			'<ogc:Literal>*' + filter[1] + '*</ogc:Literal>' +
		'</ogc:PropertyIsLike>';
	}) + '</ogc:And></ogc:Filter>'
	: ''

	var paginationLimit = 20;

	DBG && console.log('graphFetchData...', {
		paginationLimit: paginationLimit,
		page: page,
		filterIdGroup: filterIdGroup,
		filterIdRaport: filterIdRaport,
		filterFields: filterFields,
		ogcFilterFields: ogcFilterFields,
	});

	function refFieldsOnPathToList(typeName, fields) {
		var fields = fields || []
		var flatList = []
		var refPathBase = [
			'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object',
			'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row',
			'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object'
		]
		flatList.push(refPathBase.concat(typeName).join('/'))
		fields.forEach(function (fieldName) {
			flatList.push(refPathBase.concat(typeName, fieldName).join('/'))
		})
		return flatList;
	}

	return p5WFS_GetFeature(('pracownicy' === nameSection) ? 'default_db__x3A__BI_audit_ENERGA_PRACOWNICY:BI_audit_ENERGA_PRACOWNICY' : 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI:BI_audit_ENERGA_RUM_KONTRAHENCI',
		Object.assign({
			sortBy: 'ID+D',
			maxFeatures: paginationLimit,
			startIndex: (page - 1) * paginationLimit,
			// TODO: backRefNS, backRefPK, backRefField - TODO: from groups
			// resolve: 'all',
			// resolveDepth: 2
			// 'ogc:Filter': '<wfs:Query>' + '\n' + [
			// 	'ID',
			// 	'imiona',
			// 	'nazwisko',
			// 	'miejscowosc'
			// ]

			'ogc:Filter': '<wfs:Query>' + '\n' + [].concat(
				('pracownicy' === nameSection)
				? [
					'ID',
					'imiona',
					'nazwisko',
					'miejscowosc'
				] : [
					'ID',
					'Pelna_nazwa_kontrahenta'
				]
			)
			.concat(refFieldsOnPathToList('default_db__x3A__BI_audit_KRS:BI_audit_KRS', [ 'ID', 'nazwa', 'krs', 'S_miejscowosc' ]))
			.concat(refFieldsOnPathToList('default_db__x3A__BI_audit_MSIG:BI_audit_MSIG', [ 'ID', 'nazwa' ]))
			.concat(refFieldsOnPathToList('default_db__x3A__BI_audit_CEIDG:BI_audit_CEIDG', [ 'ID', 'nazwisko', 'firma' ]))
			.concat(refFieldsOnPathToList('default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI:BI_audit_ENERGA_RUM_KONTRAHENCI', [ 'ID', 'Pelna_nazwa_kontrahenta' ]))
			.concat(
				('pracownicy' === nameSection)
				? []
				: refFieldsOnPathToList('default_db__x3A__BI_audit_ENERGA_PRACOWNICY:BI_audit_ENERGA_PRACOWNICY', [ 'ID', 'imiona', 'nazwisko', 'miejscowosc' ])
			)
			.concat([
				// '*',
				// 'default_db__x3A__BI_audit_ENERGA_PRACOWNICY_adresy:BI_audit_ENERGA_PRACOWNICY_adresy/*',
				[
					'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object',
					'ID'
				].join('/'),
				[
					'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object',
					'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row',
					'ID'
				].join('/'),
				[
					'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object',
					'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row',
					'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object',
					'ID'
				].join('/')
			]).map(function (fieldName) {
				return '<wfs:PropertyName>' + fieldName + '</wfs:PropertyName>';
			}).join('\n') + '\n' +
			( ogcFilterFields ? ogcFilterFields : '' ) +
			'</wfs:Query>'
		}, ('pracownicy' === nameSection && filterIdRaport > 0)
			? {
				backRefNS: 'default_db/BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA/BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA',
				backRefPK: filterIdRaport,
				backRefField: 'default_db__x3A__BI_audit_ENERGA_PRACOWNICY:BI_audit_ENERGA_PRACOWNICY',
			}
			: {}
		, ('kontrahenci' === nameSection && filterIdRaport > 0)
			? {
				backRefNS: 'default_db/BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA/BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA',
				backRefPK: filterIdRaport,
				backRefField: 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI:BI_audit_ENERGA_RUM_KONTRAHENCI',
			}
			: {}
		, ('pracownicy' === nameSection && filterIdGroup > 0)
			? {
					backRefNS: 'default_db/BI_audit_ENERGA_PRACOWNICY_group/BI_audit_ENERGA_PRACOWNICY_group',
					backRefPK: filterIdGroup,
					backRefField: 'default_db__x3A__BI_audit_ENERGA_PRACOWNICY:BI_audit_ENERGA_PRACOWNICY',
				}
			: {}
		, ('kontrahenci' === nameSection && filterIdGroup > 0)
			? {
				backRefNS: 'default_db/BI_audit_ENERGA_PRACOWNICY_group/BI_audit_ENERGA_PRACOWNICY_group',
				backRefPK: filterIdGroup,
				backRefField: 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI:BI_audit_ENERGA_RUM_KONTRAHENCI',
			}
			: {}
		)
	);
}

function graphRender(props, wrapNode) {
	throw "TODO: graphRender..."
}

function graphRaportRender(props, wrapNode) {
	var svgNode = jQuery(wrapNode).children('svg')
	svgNode = (svgNode.length) ? d3.select(svgNode.get(0)) : d3.select(wrapNode).append("svg")
	// svg.append("rect").attr("x",0).attr("y",0).attr("width","100%").attr("height","100%").attr("fill","white").attr('class','background').on('mouseup', () => redraw())
	// svgNode.attr("width", '100%')
	// 	.attr("height", '300px')
		// .append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

	// var data = parseGraphRec(props.items, props.nameSection)
	var _todoGraphData = [];
	parseResponseRec(_todoGraphData, props.items, props.typeName)
	DBG && console.log('_todoGraphData', _todoGraphData)
	var _nodes = [];
	var _links = [];
	var mapNodeIdToIdx = {};
	_todoGraphData.forEach(function (levelData, levelIdx) {
		if (levelData.nodes && levelData.nodes.length) {
			levelData.nodes.forEach(function (node) {
				try {
					if (node.id in mapNodeIdToIdx) return;
					_nodes.push(node) // _nodes.add(node)
					mapNodeIdToIdx[node.id] = _nodes.length - 1
				} catch (e) {
					DBG && console.log('_graphData.nodes.add [level='+levelIdx+'] error:', e);
				}
			})
		}
	})

	var linkIdsAdded = []
	_todoGraphData.forEach(function (levelData, levelIdx) {
		if (levelData.edges && levelData.edges.length) {
			levelData.edges.forEach(function (edge) {
				DBG && console.log('_graphData.edges.add [level='+levelIdx+']:', {edge, source:mapNodeIdToIdx[edge.source], target:mapNodeIdToIdx[edge.target], mapNodeIdToIdx});
				// try {
					// var source = mapNodeIdToIdx[edge.source]
					// var target = mapNodeIdToIdx[edge.target]
					// if (!source || !target) { // && !== 0
					// 	console.warn('(!source || !target)', {source, target, edge, mapNodeIdToIdx})
					// }
					if (-1 !== linkIdsAdded.indexOf(edge.id)) return
					// _links.push({
					// 	id: edge.id,
					// 	source: mapNodeIdToIdx[edge.source],
					// 	target: mapNodeIdToIdx[edge.target],
					// 	value: 1
					// })
					_links.push({
						id: edge.id,
						source: edge.source,
						target: edge.target,
						value: 1
					}) // _edges.add(edge)
					linkIdsAdded.push(edge.id)
				// } catch (e) {
				// 	DBG && console.log('_graphData.edges.add [level='+levelIdx+'] error:', e);
				// }
			})
		}
	})

	var totalLinks = _links.length
	// console.log('DBG:0: _links', _links)
	DBG && console.log('DBG:1: _nodes', _nodes)
	_nodes = _nodes.filter(function (node, idx) {
		if (node.typeName !== props.typeName) return true
		for (var i=0; i<totalLinks; i++) {
			var link = _links[i]
			if (_links[i].source === node.id) return true
			if (_links[i].target === node.id) return true
		}
		return false
	})
	// console.log('DBG:2: _nodes', _nodes)
	jQuery(wrapNode).find('p').remove()
	if (!_nodes || !_nodes.length) {
		jQuery(wrapNode).find('svg').remove()
		jQuery(wrapNode).append('<p>Brak powiązań</p>')
		return;
	}

	var rightSideNodes = _nodes.filter(function (node, idx) {
		for (var i=0; i<totalLinks; i++) {
			var link = _links[i]
			// if (_links[i].source === node.id) return true
			if (_links[i].target === node.id) return true
		}
		return false
	})
	DBG && console.log('DBG: rightSideNodes (total:'+rightSideNodes.length+') ', rightSideNodes)
	if (rightSideNodes.length > 20) {
		// TODO: increase height
	}


	var graphData = {
		nodes: _nodes,
		links: _links.filter(function (link) {
			DBG && console.log('DBG loop link', { link, source: link.source, target: link.target, filtered: (link.source && link.target) });
			return (link.source && link.target);
		})
	};

	DBG && console.warn('render graphData', graphData);
	var graf = renderGraph(svgNode, graphData, {
		width: jQuery(wrapNode).width(),
		height: (rightSideNodes.length > 20) ? rightSideNodes.length * 22 : 500
	})
	graf.on('click', (event) => {
	  DBG && console.log('event', event)
	  // event = {
	  //   nativeEvent: d3.event,
	  //   node: a,
	  //   element: d3.event.srcElement
	  // }

	  // TODO: !a.expanded && redraw({type: a.name, criteria: 'ID'})
	})
}

/**
 * Response format:
 * NOTE: every object could be xlink if found before
 * lvl1      > lvl2         > lvl3  > lvl4         > lvl5
 * Pracownik > *_row_object > *_row > *_row_object > Podmiot
 * Pracownik > *_row_object > *_row > *_row_object > ( KRS | CEIDG | ... )
 * Pracownik > *_row_object > *_row > *_row_object > Pracownik
 *
 * Podmiot > *_row_object > *_row > *_row_object > Pracownik
 * Podmiot > *_row_object > *_row > *_row_object > ( KRS | CEIDG | ... )
 * Podmiot > *_row_object > *_row > *_row_object > Podmiot
 *
 * TODO: make connections only: Pracownik -> ( KRS | CEIDG | ... )* -> Podmiot
 * NOTE: lvl4 last `_row_object` is (wfs xlink cache system) xlink to lvl2 `_row_object` - ref to lvl1 object
 * NOTE: lvl3 is always 1 because it is from __backRef
 */
var TN_PRACOWNICY = 'default_db__x3A__BI_audit_ENERGA_PRACOWNICY:BI_audit_ENERGA_PRACOWNICY';
var TN_KONTRAHENCI = 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI:BI_audit_ENERGA_RUM_KONTRAHENCI';
var TN_ROW_OBJ = 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object';
var TN_ROW = 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row';
var TN_KRS = 'default_db__x3A__BI_audit_KRS:BI_audit_KRS';
var TN_CEIDG = 'default_db__x3A__BI_audit_CEIDG:BI_audit_CEIDG';
var TN_MSIG = 'default_db__x3A__BI_audit_MSIG:BI_audit_MSIG';
function parseResponseRec__Helper__addTodoNode(_todoGraphData, level, json, typeName) { // @return string - node id
	if (!_todoGraphData[level]) _todoGraphData[level] = { nodes: [], edges: [] }
	var nodeObject = dataMakeNode({
		typeName: typeName,
		primaryKey: (json['ID']) ? json['ID'] : null, // TODO: get primaryKey from object
		row: json,
	})
	_todoGraphData[level].nodes.push(nodeObject)
	// if (parentNodeId) {
	// 	_todoGraphData[level].edges.push(dataMakeEdge(parentNodeId, nodeObject))
	// }
	return nodeObject.id;
}
function parseResponseRec__Helper__addTodoEdge(_todoGraphData, level, parentNodeId, newNodeId) {
	if (!_todoGraphData[level]) _todoGraphData[level] = { nodes: [], edges: [] }
	_todoGraphData[level].edges.push(
		// dataMakeEdge(parentNodeId, nodeObject)
		{
			id: parentNodeId + newNodeId,
			source: parentNodeId,
			target: newNodeId,
		}
	)
}
function parseResponseRec(_todoGraphData, json, typeName) {
	DBG = 1;
	var parentNodeId = null
	DBG && console.log('DBG::parseResponseRec', {json:json, typeName:typeName, parentNodeId:parentNodeId, isString: p5Utils__isString(json), isArray: p5Utils__isArray(json), isObject: p5Utils__isObject(json)});
	switch (typeName) {
		case TN_PRACOWNICY: parseResponseRec__lvl1__Pracownik(_todoGraphData, json, typeName); break;
		case TN_KONTRAHENCI: parseResponseRec__lvl1__Kontrahent(_todoGraphData, json, typeName); break;
		default: throw "Not implemented root element typeName '" + typeName + "'";
	}
}
function parseResponseRec__lvl1__Pracownik(_todoGraphData, jsonList, typeName) {
	if (!p5Utils__isArray(jsonList)) throw "Not implemented root element - expected list of elements";
	var isXlinkList = (jsonList.length > 0 && p5Utils__isString(jsonList[0]))
	DBG && console.log('DBG::parseResponseRec__lvl1__Pracownik', {jsonList:jsonList, typeName:typeName, isString: p5Utils__isString(jsonList), isArray: p5Utils__isArray(jsonList), isObject: p5Utils__isObject(jsonList), isXlinkList});
	jsonList.forEach(function (jsonPracownik) {
		if (isXlinkList) {
			DBG && console.warn('DBG::parseResponseRec__lvl1__Pracownik', {jsonPracownik:jsonPracownik, typeName:typeName, isString: p5Utils__isString(jsonPracownik), isArray: p5Utils__isArray(jsonPracownik), isObject: p5Utils__isObject(jsonPracownik), isXlinkList});
			// parseResponseXlinkListRec(_todoGraphData, jsonPracownik, typeName)
			throw "Not implemented xlink in Pracownik child";
		} else {
			var idNode = parseResponseRec__Helper__addTodoNode(_todoGraphData, level = 1, jsonPracownik, typeName);
			if (jsonPracownik[TN_ROW_OBJ] && jsonPracownik[TN_ROW_OBJ].length) {
				parseResponseRec__lvl2__Pracownik_RowObj(_todoGraphData, idNode, jsonPracownik[TN_ROW_OBJ]);
			}
		}
	})
}
function parseResponseRec__lvl2__Pracownik_RowObj(_todoGraphData, parentIdNode, rowObjList) {
	DBG && console.log('DBG::parseResponseRec__lvl2__Pracownik_RowObj rec...', { parentIdNode, rowObjList });
	rowObjList.forEach(function (jsonRowObj) {
		if (!p5Utils__isObject(jsonRowObj)) {
			DBG && console.warn('DBG::parseResponseRec__lvl2__Pracownik_RowObj SKIP non object element', { jsonRowObj });
			// skip non object elements
			return;
		}
		if (isP5LinkObject(jsonRowObj)) {
			DBG && console.log('DBG::parseResponseRec__lvl2__Pracownik_RowObj SKIP xlink', { jsonRowObj });
			// skip p5 link object (type: "next")
			return;
		}
		DBG && console.log('DBG::parseResponseRec__lvl2__Pracownik_RowObj row obj', { jsonRowObj });
		if (jsonRowObj[TN_ROW] && jsonRowObj[TN_ROW].length) {
			// NOTE: jsonRowObj[TN_ROW] is array(1) - always 1 because it is from __backRef
			parseResponseRec__lvl3__Pracownik_RowObj_Row(_todoGraphData, parentIdNode, jsonRowObj[TN_ROW][0]);
		}
	})
}
function parseResponseRec__lvl3__Pracownik_RowObj_Row(_todoGraphData, parentIdNode, jsonRow) {
	DBG && console.log('DBG::parseResponseRec__lvl3__Pracownik_RowObj_Row', { jsonRow, parentIdNode });
	if (jsonRow[TN_ROW_OBJ] && jsonRow[TN_ROW_OBJ].length) {
		var jsonListRowObj = jsonRow[TN_ROW_OBJ];
		DBG && console.log('DBG::parseResponseRec__lvl3__Pracownik_RowObj_Row row obj', { jsonListRowObj, parentIdNode });
		// NOTE: RowObj on list is a connection path in reverse order
		// NOTE: last element is Pracownik (parentIdNode)
		// NOTE: first element is Kontrahent - TODO: or another element defines in Raport.od.* or Raport.do.*
		// var TN_PRACOWNICY = 'default_db__x3A__BI_audit_ENERGA_PRACOWNICY:BI_audit_ENERGA_PRACOWNICY';
		// var TN_KONTRAHENCI = 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI:BI_audit_ENERGA_RUM_KONTRAHENCI';
		// var TN_ROW_OBJ = 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object';
		// var TN_ROW = 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row';
		// var TN_KRS = 'default_db__x3A__BI_audit_KRS:BI_audit_KRS';
		// var TN_CEIDG = 'default_db__x3A__BI_audit_CEIDG:BI_audit_CEIDG';
		// var TN_MSIG = 'default_db__x3A__BI_audit_MSIG:BI_audit_MSIG';
		var pathPoints = jsonListRowObj.reverse().slice(1).map(function (jsonRowObj) {
			if (jsonRowObj[TN_KONTRAHENCI]) return Object.assign(jsonRowObj[TN_KONTRAHENCI][0], { typeName: TN_KONTRAHENCI });
			if (jsonRowObj[TN_PRACOWNICY]) return Object.assign(jsonRowObj[TN_PRACOWNICY][0], { typeName: TN_PRACOWNICY });
			if (jsonRowObj[TN_KRS]) return Object.assign(jsonRowObj[TN_KRS][0], { typeName: TN_KRS });
			if (jsonRowObj[TN_CEIDG]) return Object.assign(jsonRowObj[TN_CEIDG][0], { typeName: TN_CEIDG });
			if (jsonRowObj[TN_MSIG]) return Object.assign(jsonRowObj[TN_MSIG][0], { typeName: TN_MSIG });
			return null;
		}).filter(function (jsonRowObj) {
			return !!jsonRowObj;
		}).map(function (jsonRowObj) {
			DBG && console.warn('DBG::parseResponseRec__lvl3__Pracownik_RowObj_Row path', { parentIdNode, fid: (jsonRowObj ? jsonRowObj.typeName + '.' + jsonRowObj.ID : null), jsonRowObj });
			return jsonRowObj;
		})
		for (var i = 0, prevNodeId = parentIdNode, newNodeId; i < pathPoints.length; i++) {
			jsonObject = pathPoints[i];
			newNodeId = parseResponseRec__Helper__addTodoNode(_todoGraphData, level = 4, jsonObject, jsonObject.typeName);
			parseResponseRec__Helper__addTodoEdge(_todoGraphData, level = 4, prevNodeId, newNodeId);
			prevNodeId = newNodeId;
		}
	}
}

function parseResponseRec__lvl1__Kontrahent(_todoGraphData, jsonList, typeName) {
	if (!p5Utils__isArray(jsonList)) throw "Not implemented root element - expected list of elements";
	var isXlinkList = (jsonList.length > 0 && p5Utils__isString(jsonList[0]))
	DBG && console.log('DBG::parseResponseRec__lvl1__Kontrahent', {jsonList:jsonList, typeName:typeName, isString: p5Utils__isString(jsonList), isArray: p5Utils__isArray(jsonList), isObject: p5Utils__isObject(jsonList), isXlinkList});
	jsonList.forEach(function (jsonPracownik) {
		if (isXlinkList) {
			DBG && console.warn('DBG::parseResponseRec__lvl1__Kontrahent', {jsonPracownik:jsonPracownik, typeName:typeName, isString: p5Utils__isString(jsonPracownik), isArray: p5Utils__isArray(jsonPracownik), isObject: p5Utils__isObject(jsonPracownik), isXlinkList});
			// parseResponseXlinkListRec(_todoGraphData, jsonPracownik, typeName)
			throw "Not implemented xlink in Pracownik child";
		} else {
			var idNode = parseResponseRec__Helper__addTodoNode(_todoGraphData, level = 1, jsonPracownik, typeName);
			if (jsonPracownik[TN_ROW_OBJ] && jsonPracownik[TN_ROW_OBJ].length) {
				parseResponseRec__lvl2__Kontrahent_RowObj(_todoGraphData, idNode, jsonPracownik[TN_ROW_OBJ]);
			}
		}
	})
}
function parseResponseRec__lvl2__Kontrahent_RowObj(_todoGraphData, parentIdNode, rowObjList) {
	DBG && console.log('DBG::parseResponseRec__lvl2__Kontrahent_RowObj rec...', { parentIdNode, rowObjList });
	rowObjList.forEach(function (jsonRowObj) {
		if (!p5Utils__isObject(jsonRowObj)) {
			DBG && console.warn('DBG::parseResponseRec__lvl2__Kontrahent_RowObj SKIP non object element', { jsonRowObj });
			// skip non object elements
			return;
		}
		if (isP5LinkObject(jsonRowObj)) {
			DBG && console.log('DBG::parseResponseRec__lvl2__Kontrahent_RowObj SKIP xlink', { jsonRowObj });
			// skip p5 link object (type: "next")
			return;
		}
		DBG && console.log('DBG::parseResponseRec__lvl2__Kontrahent_RowObj row obj', { jsonRowObj });
		if (jsonRowObj[TN_ROW] && jsonRowObj[TN_ROW].length) {
			// NOTE: jsonRowObj[TN_ROW] is array(1) - always 1 because it is from __backRef
			parseResponseRec__lvl3__Kontrahent_RowObj_Row(_todoGraphData, parentIdNode, jsonRowObj[TN_ROW][0]);
		}
	})
}
function parseResponseRec__lvl3__Kontrahent_RowObj_Row(_todoGraphData, parentIdNode, jsonRow) {
	DBG && console.log('DBG::parseResponseRec__lvl3__Pracownik_RowObj_Row', { jsonRow, parentIdNode });
	if (jsonRow[TN_ROW_OBJ] && jsonRow[TN_ROW_OBJ].length) {
		var jsonListRowObj = jsonRow[TN_ROW_OBJ];
		DBG && console.log('DBG::parseResponseRec__lvl3__Kontrahent_RowObj_Row row obj', { jsonListRowObj, parentIdNode });
		// NOTE: RowObj on list is a connection path in reverse order
		// NOTE: last element is Pracownik (parentIdNode)
		// NOTE: first element is Kontrahent - TODO: or another element defines in Raport.od.* or Raport.do.*
		// var TN_PRACOWNICY = 'default_db__x3A__BI_audit_ENERGA_PRACOWNICY:BI_audit_ENERGA_PRACOWNICY';
		// var TN_KONTRAHENCI = 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI:BI_audit_ENERGA_RUM_KONTRAHENCI';
		// var TN_ROW_OBJ = 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object';
		// var TN_ROW = 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row';
		// var TN_KRS = 'default_db__x3A__BI_audit_KRS:BI_audit_KRS';
		// var TN_CEIDG = 'default_db__x3A__BI_audit_CEIDG:BI_audit_CEIDG';
		// var TN_MSIG = 'default_db__x3A__BI_audit_MSIG:BI_audit_MSIG';
		var pathPoints = jsonListRowObj.slice(1).reverse().map(function (jsonRowObj) {
			if (jsonRowObj[TN_KONTRAHENCI]) return Object.assign(jsonRowObj[TN_KONTRAHENCI][0], { typeName: TN_KONTRAHENCI });
			if (jsonRowObj[TN_PRACOWNICY]) return Object.assign(jsonRowObj[TN_PRACOWNICY][0], { typeName: TN_PRACOWNICY });
			if (jsonRowObj[TN_KRS]) return Object.assign(jsonRowObj[TN_KRS][0], { typeName: TN_KRS });
			if (jsonRowObj[TN_CEIDG]) return Object.assign(jsonRowObj[TN_CEIDG][0], { typeName: TN_CEIDG });
			if (jsonRowObj[TN_MSIG]) return Object.assign(jsonRowObj[TN_MSIG][0], { typeName: TN_MSIG });
			return null;
		}).filter(function (jsonRowObj) {
			return !!jsonRowObj;
		}).map(function (jsonRowObj) {
			DBG && console.warn('DBG::parseResponseRec__lvl3__Kontrahent_RowObj_Row path', { parentIdNode, fid: (jsonRowObj ? jsonRowObj.typeName + '.' + jsonRowObj.ID : null), jsonRowObj });
			return jsonRowObj;
		})
		for (var i = 0, prevNodeId, newNodeId; i < pathPoints.length; i++) {
			jsonObject = pathPoints[i];
			newNodeId = parseResponseRec__Helper__addTodoNode(_todoGraphData, level = 4, jsonObject, jsonObject.typeName);
			if (prevNodeId) parseResponseRec__Helper__addTodoEdge(_todoGraphData, level = 4, prevNodeId, newNodeId);
			prevNodeId = newNodeId;
		}
		{
			parseResponseRec__Helper__addTodoEdge(_todoGraphData, level = 4, prevNodeId, parentIdNode);
		}
	}
}


function parseResponseRec__OLD(_todoGraphData, json, typeName, parentNodeId, level) {
	var level = level || 0
	var parentNodeId = parentNodeId || null
	DBG && console.log('DBG::parseResponseRec', {json:json, typeName:typeName, parentNodeId:parentNodeId, isString: p5Utils__isString(json), isArray: p5Utils__isArray(json), isObject: p5Utils__isObject(json)});
	if (p5Utils__isArray(json)) {
		// TODO: create named group
		var isXlinkList = (json.length > 0 && p5Utils__isString(json[0]))
		json.forEach(function (subJson) {
			if (isXlinkList) {
				parseResponseXlinkListRec(_todoGraphData, subJson, typeName, parentNodeId, level)
			} else {
				parseResponseRec(_todoGraphData, subJson, typeName, parentNodeId, level)
			}
		})
	} else if (p5Utils__isObject(json) && isP5LinkObject(json)) {
		DBG && console.log('DBG::parseResponseRec isP5LinkObject');
		parseResponseP5Link(_todoGraphData, json, typeName, parentNodeId, level)
	} else if (p5Utils__isObject(json)) {
		// _todoGraphData.nodes.add({ id: nodeId, label: nodeId })
		if (!_todoGraphData[level]) _todoGraphData[level] = { nodes: [], edges: [] }
		var nodeObject = dataMakeNode({
			typeName: typeName,
			primaryKey: (json['ID']) ? json['ID'] : null, // TODO: get primaryKey from object
			row: json,
		})
		var nodeId = nodeObject.id

		if ("default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row" === typeName) {
			parseResponseRec__row(_todoGraphData, json, typeName, parentNodeId, level)
		}
		else if ("default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object" === typeName) {
			// parseResponseRec__row_object(_todoGraphData, json, typeName, parentNodeId, level)
			Object.keys(json).filter(function (fieldName) {
				return (fieldName.indexOf(':') > -1)
			})
			.forEach(function (fieldName) {
				var value = json[fieldName]
				parseResponseRec(_todoGraphData, value, fieldName, parentNodeId, level + 1)
			})
		}
		else {
			DBG && console.warn('DBG::parseResponseRec: obj node.push', { id: nodeObject.id, parentNodeId, typeName, nodeObject });
			_todoGraphData[level].nodes.push(nodeObject)
			if (parentNodeId) {
				_todoGraphData[level].edges.push(dataMakeEdge(parentNodeId, nodeObject))
			}
			Object.keys(json).filter(function (fieldName) {
				return (fieldName.indexOf(':') > -1)
			})
			.forEach(function (fieldName) {
				var value = json[fieldName]
				parseResponseRec(_todoGraphData, value, fieldName, nodeId, level + 1)
			})
		}
	} else if (p5Utils__isString(json)) {
		DBG && console.log('TODO: Not implemented - parseResponseRec isString');
	} else {
		DBG && console.log('TODO: Not implemented - parseResponseRec is not string, not array and not object');
	}
}
function parseResponseXlinkListRec(_todoGraphData, json, typeName, parentNodeId, level) {
	DBG && console.log('DBG::parseResponseRec:XlinkList', {json:json, typeName:typeName, parentNodeId:parentNodeId, isString: p5Utils__isString(json), isArray: p5Utils__isArray(json), isObject: p5Utils__isObject(json)});
	if (p5Utils__isString(json)) { // xlink "https://biuro.biall-net.pl/wfs/default_db/BI_audit_ENERGA_RUM_KONTRAHENCI#BI_audit_ENERGA_RUM_KONTRAHENCI.9233",
		if ("default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object" === typeName) return;
		if ("default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row" === typeName) return;
		var nodeId = json.substr(json.indexOf('#') + 1)
		{
			// _graphData.nodes.add({ id: nodeId, label: nodeId })
			if (!_todoGraphData[level]) _todoGraphData[level] = { nodes: [], edges: [] }
			var nodeObject = dataMakeXlinkNode({
				xlink: json,
				typeName: typeName,
			})
			DBG && console.warn('DBG::parseResponseRec: xlink node.push', { id: nodeObject.id, parentNodeId, typeName, nodeObject });
			_todoGraphData[level].nodes.push(nodeObject)
			if (parentNodeId) {
				_todoGraphData[level].edges.push(dataMakeEdge(parentNodeId, nodeObject))
			}
		}
	} else if (p5Utils__isObject(json) && isP5LinkObject(json)) {
		parseResponseP5Link(_todoGraphData, json, typeName, parentNodeId, level)
	} else if (p5Utils__isObject(json)) {
		DBG && console.warn('TODO: Not implemented - parseResponseRec:XlinkList is object', {json:json, typeName:typeName, parentNodeId:parentNodeId, isString: p5Utils__isString(json), isArray: p5Utils__isArray(json), isObject: p5Utils__isObject(json)});
		parseResponseRec(_todoGraphData, json, typeName, parentNodeId, level + 1)
	} else {
		DBG && console.warn('TODO: Not implemented - parseResponseRec:XlinkList is not string and not object');
	}
}
function parseResponseP5Link(_todoGraphData, json, typeName, parentNodeId, level) {
	DBG && console.log('parseResponseRec isObject and P5Link - fetch more xlink object');
	// example json: { type: "next",
	//  @backRefNS: "default_db/BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA/BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA",
	//  @backRefPK: "42",
	//  @typeName: "default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_P…ow:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row",
	//  @startIndex: "10" }
	if (!parentNodeId) throw "Missing parentNodeId for ref link object";
	var objectName = typeName.substr(typeName.indexOf(':') + 1)
	{
		// _graphData.nodes.add({ id: nodeId, label: nodeId })
		if (!_todoGraphData[level]) _todoGraphData[level] = { nodes: [], edges: [] }
		switch (json.type) {
			case 'next': {
        // TODO: add fetch next node
				// var nodeObject = dataMakeFetchMoreNode({
				// 	parentNodeId: parentNodeId,
				// 	type: json.type,
				// 	objectName: objectName,
				// 	ref: json,
				// })
				// _todoGraphData[level].nodes.push(nodeObject)
				// _todoGraphData[level].edges.push(dataMakeFetchMoreEdge(parentNodeId, nodeObject))
			} break;
			default: {
				DBG && console.log('TODO: Not implemented - parseResponseRec isObject with type "'+json.type+'" - fetch more xlink object');
			}
		}
	}
}

function parseResponseRec__row(_todoGraphData, json, typeName, parentNodeId, level) {
	var nodeObject = dataMakeNode({
		typeName: typeName,
		primaryKey: (json['ID']) ? json['ID'] : null, // TODO: get primaryKey from object
	})
	var nodeId = nodeObject.id

	// 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row',
	// 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object',
	// 'default_db__x3A__BI_audit_KRS:BI_audit_KRS' --> Kontrahenci
	var rowObjectChildrens = []
	var rowTN = 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row'
	var rowObjTN = 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object'
	// { row: row_obj: [ { ID, kontr: [ "http...kontr.ID" ] }, { ID, kontr: [ "http...krs.ID" ] } ], ... }
	var rowJson = json
	if (rowJson[rowObjTN] && rowJson[rowObjTN].length > 1) {
		var pathItems = []
		rowJson[rowObjTN].forEach(function (rowObjJson) {
			Object.keys(rowObjJson)
				.filter(function (rowObjFld) { return (-1 !== rowObjFld.indexOf(':')); })
				.filter(function (rowObjRefFld) { return (rowObjJson[rowObjRefFld] && rowObjJson[rowObjRefFld].length); })
				.forEach(function (rowObjRefFld) {
					pathItems.push([ rowObjRefFld, rowObjJson[rowObjRefFld][0] ])
				})
		})
	}
	// console.log('node('+nodeId+') pathItems:', pathItems);
	var lastParentFid = null
	pathItems.reverse().forEach(function (pathItem) {
		var nodeObject = ('string' === typeof pathItem[1])
		? dataMakeXlinkNode({
				xlink: pathItem[1],
				typeName: pathItem[0]
			})
		:	 dataMakeNode({
				typeName: pathItem[0],
				primaryKey: (pathItem[1]['ID']) ? pathItem[1]['ID'] : null,
				row: pathItem[1]
			})
		;
		// _todoGraphData[todoLevel].nodes.push(nodeObject) // already added below
		if (lastParentFid) _todoGraphData[level].edges.push(dataMakeEdge(lastParentFid, nodeObject))
		lastParentFid = nodeObject.id
	})

	Object.keys(json).filter(function (fieldName) {
		return (fieldName.indexOf(':') > -1)
	})
	.forEach(function (fieldName) {
		var value = json[fieldName]
		parseResponseRec(_todoGraphData, value, fieldName, parentNodeId, level + 1)
	})
}

function parseGraphRec(items, featureType, parentFeatureId) { // TODO: not used
	var parentFeatureId = parentFeatureId || null
	var nodesArray = []
	var linksArray = []
	if (!items) return;
	items.forEach(function (item) {
		if ('string' === typeof item) {
			DBG && console.log('TODO: xlink "'+item+'": ', {item:item, parentFeatureId:parentFeatureId})
			return;
		}
		if (!item['ID']) {
			DBG && console.log('TODO: SKIP item('+featureType+') - missing ID: ', {item:item, parentFeatureId:parentFeatureId})
			return;
		}
		var id = item['ID']
		console.log('item('+featureType+'): ', {item:item, parentFeatureId:parentFeatureId})
		var featureId = featureType + '.' + id
		Object.keys(item).filter(function (fieldName) { return ('ID' !== fieldName); })
		.forEach(function (fieldName) {
			parseGraphRec(item[fieldName], fieldName, featureId)
		})
	})
  // items.forEach(element => {
  //   const rowObject = hasTargetArray(element);
  //   if (rowObject)
  //     rowObject.array.forEach(objectElement => {
  //       const trackNode = hasTargetArray(objectElement);
  //       if (trackNode) {
  //         let group = trackNode.name;
  //         let expanded = false;
  //         if (expansion && expansion.type && group === expansion.type) {
  //           group = trackNode.array[0][expansion.criteria];
  //           expanded = true;
  //         }
  //         nodes[group] = {expanded: expanded, type: trackNode.name};
  //         instanceTypes[trackNode.array[0].ID] = group;
  //       }
  //     });
  // });

	return {
		nodes: nodesArray,
		links: linksArray
	}
}


function isP5LinkObject(json) {
	if ( !('type' in json) || !json['type'] ) return false;
	if ( !('value' in json) || !json['value'] ) return false;
	if ( !('@typeName' in json) || !json['@typeName'] ) return false;
	if ( !('@startIndex' in json) || !json['@startIndex'] ) return false;
	if ( !('@backRefPK' in json) || !json['@backRefPK'] ) return false;
	if ( !('@backRefNS' in json) || !json['@backRefNS'] ) return false;
	return true;
}

function dataMakeNode(params) {
	var objectName = params.typeName.substr(params.typeName.indexOf(':') + 1)
	var nodeId = objectName + '.' + params.primaryKey // TODO: primaryKey?
	var graphNode = {
		id: nodeId,
		name: nodeId,
		group: objectName,
		_loaded: true,
		typeName: params.typeName,
		primaryKey: params.primaryKey // TODO: _primaryKey
	}
	if (params.row) graphNode.row = params.row
	return graphAddNodeLabel(graphNode)
}
function dataMakeEdge(parentNodeId, nodeObject) {
	return {
		id: parentNodeId + nodeObject.id,
		source: parentNodeId,
		target: nodeObject.id,
	}
}
function dataMakeXlinkNode(params) {
	var objectName = params.typeName.substr(params.typeName.indexOf(':') + 1)
	var nodeId = params.xlink.substr(params.xlink.indexOf('#') + 1)
	var primaryKey = nodeId.substr(nodeId.lastIndexOf('.') + 1)
	var graphNode = {
		id: nodeId,
		name: nodeId,
		group: objectName,
		_loaded: false,
		typeName: params.typeName,
		primaryKey: primaryKey
	}
	return graphAddNodeLabel(graphNode)
}

function dataMakeFetchMoreNode(params) {
	// params = {
	// 	parentNodeId: parentNodeId,
	// 	type: json.type,
	// 	objectName: objectName,
	// 	ref: json,
	// }
	DBG && console.log('DBG dataMakeFetchMoreNode(params)', params)
	var nodeId = params.parentNodeId+'fetch-more-features-'+params.type+'-on-' + params.objectName;
	return {
		id: nodeId,
		label: 'Pobierz więcej (+)',
		group: 'fetch-more-data', // params.objectName,
		_loaded: false,
		_type: 'ref',
		parentNodeId: params.parentNodeId,
		ref: params.ref,
		// typeName: typeName,
		// primaryKey: nodeId.substr(nodeId.lastIndexOf('.') + 1)
	};
}
function dataMakeFetchMoreEdge(parentNodeId, fetchMoreNode) {
	// @param parentNodeId - from node id
	// @param fetchMoreNode - from dataMakeFetchMoreNode
	DBG && console.log('DBG dataMakeFetchMoreEdge(parentNodeId, fetchMoreNode)', {parentNodeId:parentNodeId, fetchMoreNode:fetchMoreNode})
	return {
		id: fetchMoreNode.id,
		from: parentNodeId,
		to: fetchMoreNode.id
	}
}
function makeShortLabel(label) { // TODO: shorter name
	// BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object.1234567

	if (label.length < 30) return label;
	var exLabel = label.split('.')
	if (exLabel.length !== 2) return label;
	return exLabel[0].substr(0, 24) + '...' + exLabel[0].substr(-10) + '.' + exLabel[1];
}
function graphAddNodeLabel(graphNode) {
	// var graphNode = {
	// 	id: nodeId,
	// 	name: nodeId,
	// 	group: objectName,
	// 	_loaded: true,
	// 	typeName: params.typeName,
	// 	primaryKey: params.primaryKey // TODO: _primaryKey
	// }
	var label = graphNode.id // TODO: get from schema assert @label attribute
	label = makeShortLabel(graphNode.id)
	switch (graphNode.typeName) {
		case 'default_db__x3A__BI_audit_KRS:BI_audit_KRS': {
			// <xs:assert test="@default_db__x3A__BI_audit_KRS:label = concat(nazwa, ' ', krs, ' ', S_miejscowosc)" id="I_audit_KRS___d6e76977-1">
			if (graphNode.row) label = [graphNode.row.nazwa, graphNode.row.krs, graphNode.row.S_miejscowosc].filter(function (val) { return val; }).join(' ');
			label = (label) ? label : makeShortLabel(graphNode.id)
			label = label + ' (krs)'
		} break;
		case 'default_db__x3A__BI_audit_ENERGA_PRACOWNICY:BI_audit_ENERGA_PRACOWNICY': {
			// <xs:assert id="_PRACOWNICY___d6e76324-1" test="@label = concat(imiona, ' ', nazwisko, ' ', miejscowosc)"/>
			if (graphNode.row) label = [graphNode.row.imiona, graphNode.row.nazwisko, graphNode.row.miejscowosc].filter(function (val) { return val; }).join(' ');
			label = (label) ? label : makeShortLabel(graphNode.id)
			label = label + ' (pracownicy)'
		} break;
		case 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI:BI_audit_ENERGA_RUM_KONTRAHENCI': {
			// <xs:assert id="KONTRAHENCI___d6e76526-1" test="@label = concat(substr(Pelna_nazwa_kontrahenta, 0, 20, '\n umów na kwotę ', @sum))"/>
			if (graphNode.row) label = [graphNode.row.Pelna_nazwa_kontrahenta].filter(function (val) { return val; }).join(' ');
			label = (label) ? label : makeShortLabel(graphNode.id)
			label = label + ' (kontrahent)'
		} break;
		case 'default_db__x3A__BI_audit_CEIDG:BI_audit_CEIDG': {
			// <xs:assert test="@label = concat(nazwisko, substring(firma, 1, 20))"/>
			if (graphNode.row) label = [graphNode.row.nazwisko, graphNode.row.firma].filter(function (val) { return val; }).join(' ');
			label = (label) ? label : makeShortLabel(graphNode.id)
			label = label + ' (ceidg)'
		} break;
		case 'default_db__x3A__BI_audit_MSIG:BI_audit_MSIG': {
			// <xs:assert test="@label = substring(nazwa, 1, 20)"/>
			if (graphNode.row) label = [graphNode.row.nazwa].filter(function (val) { return val; }).join(' ');
			label = (label) ? label : makeShortLabel(graphNode.id)
			label = label + ' (msig)'
		} break;
	}
	return Object.assign(graphNode, {
		label: label,
	})
}


module.exports = {
	graphShowHide: graphShowHide,
	graphRender: graphRender,
	graphRaportRender: graphRaportRender,
	/** @example: graphRaportRender({
	 *		msg: "Pobrano dane",
	 *		typeName: ('pracownicy' === nameSection) ? 'default_db__x3A__BI_audit_ENERGA_PRACOWNICY:BI_audit_ENERGA_PRACOWNICY' : 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI:BI_audit_ENERGA_RUM_KONTRAHENCI',
	 *		items: items
	 *	}, graphResultNode)
	 */
}