// plugin/vue-renderer/core/nc-util.js
// ../core/nc-util.js
// ../../core/nc-util.js
// TODO: make core and full versions of this file, core = only those function used to bootstrap app

import peg from './nc-peg.js'
// import { currentUrl } from '/src/components/nc-router/nc-router.js'
let gridApi // import gridApi from '/src/components/nc-grid/nc-grid-api.js'

const nc = {}
nc.grid = gridApi // we must set grid api here because grid area may be not loaded before api is used
export default nc

const lineMaxLen = 45
const wsLookup = 15 // Look backwards n characters for a whitespace
const regex = new RegExp(String.raw`\s*(?:(\S{${lineMaxLen}})|([\s\S]{${lineMaxLen - wsLookup},${lineMaxLen}})(?!\S))`, 'g')

/* const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
while (!xxx) await sleep(100) */

export function round(num, decimals) {
	return Math.round((num + Number.EPSILON) * Math.pow(10, decimals)) / Math.pow(10, decimals)
}
nc.round = round

export function percent(num1, num2, decimals) {
	if (num2 !== 0) {
		return round((num1 / num2) * 100, decimals || 2)
	}
	return 0
}
nc.percent = percent

export function createIndex(data, key, exclude, noWarning) {
	let ret = {}
	let warnKey
	let val, item
	for (let i = 0; i < data.length; i++) {
		item = data[i]
		val = item[key]
		if (val != null) {
			if (exclude == null || exclude[val] == null || !exclude[val]) {
				if (ret[val] != null) {
					if (!noWarning) {
						if (warnKey == null) {
							warnKey = {}
						}
						if (warnKey[val] == null) {
							warnKey[val] = 1
							console.error(`fn.util.createIndex key '${key}' value '${val}' is not unique`)
						} else {
							warnKey[val] = warnKey[val] + 1
						}
					}
				} else {
					ret[val] = item
				}
			}
		}
	}
	if (warnKey) {
		return { index: ret, warning: warnKey }
	}
	return { index: ret }
}
nc.createIndex = createIndex

export function createIndexArray(data, key, exclude, include) {
	let ret = {}
	if (!data) {
		console.error('create index array data is null')
		return ret
	}
	let val, item
	for (let i = 0; i < data.length; i++) {
		item = data[i]
		val = item[key]
		if (val != null) {
			if ((include == null || include[val]) && (exclude == null || exclude[val] == null || !exclude[val])) {
				if (ret[val] == null) {
					ret[val] = []
				}
				ret[val].push(item)
			}
		}
	}
	return ret
}
nc.createIndexArray = createIndexArray

function arrayToIdx(tbl) {
	if (!tbl) {
		return {} // must return table, not null
	}
	const ret = {}
	tbl.forEach((item, idx) => {
		ret[item] = idx + 1 // don't use 0 because it's falsy value
	})
	return ret
}
nc.arrayToIdx = arrayToIdx

export function splitTextToWidth(text) {
	return text.replace(regex, (_, x, y) => (x ? `${x}-\n` : `${y}\n`))
}
nc.splitTextToWidth = splitTextToWidth

export function binarySearch(array, compareFn, startIndex = 0) {
	// Return 0 <= i <= array.length such that !compareFn(array[i - 1]) && compareFn(array[i]).
	// copied from: https://stackoverflow.com/questions/22697936/binary-search-in-javascript
	let lo = startIndex - 1,
		hi = array.length // max value to return
	while (lo + 1 < hi) {
		const mid = lo + ((hi - lo) >> 1)
		if (compareFn(array[mid])) {
			hi = mid
		} else {
			lo = mid
		}
	}
	return hi
}
nc.binarySearch = binarySearch
// nc.binarySearch = binarySearch

export function clone(input, doNotCloneFields) {
	// copied from: https://medium.com/better-programming/javascript-tips-2-object-array-deep-clone-implementation-2d6a43e43d2a
	if (input == null || typeof input != 'object') return input
	const output = Array.isArray(input) ? [] : {}
	for (const key of Object.keys(input)) {
		if (doNotCloneFields == null || !doNotCloneFields.includes(key)) {
			output[key] = clone(input[key], doNotCloneFields)
		}
	}
	return output
}
nc.clone = clone

export function invertTable(array) {
	if (array == null) {
		return array
	}
	const ret = {}
	for (let i = 0; i < array.length; i++) {
		ret[array[i]] = i + 1 // starting from 1 (0 is falsy value)
	}
	return ret
}
nc.invertTable = invertTable
/* nc.onIdle = function (cb = () => {}) {
	if ('requestIdleCallback' in window) {
		window.requestIdleCallback(cb)
	} else {
		setTimeout(() => {
			nextTick(cb)
		}, 300)
	}
} */

/*
nc.generateUUID = function() { // Public Domain/MIT
	// https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
	let d = new Date().getTime()//Timestamp
	let d2 = (performance && performance.now && (performance.now() * 1000)) || 0//Time in microseconds since page-load or 0 if unsupported
	return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
		let r = Math.random() * 16//random number between 0 and 16
		if (d > 0) {//Use timestamp until depleted
			r = (d + r) % 16 | 0
			d = Math.floor(d / 16)
		} else {//Use microseconds since page-load if supported
			r = (d2 + r) % 16 | 0
			d2 = Math.floor(d2 / 16)
		}
		return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
	})
}
*/

nc.todo = function (msg) {
	console.debug('todo: ' + msg)
}

/* nc.escapeHtml = function(html) {
	return html
		.replace(/&/g, '&amp;')
		.replace(/</g, '&lt;')
		.replace(/>/g, '&gt;')
		.replace(/"/g, '&quot;')
		.replace(/'/g, '&#039;')
} */

nc.zipObject = function (props, values) {
	return props.reduce((prev, prop, i) => {
		return Object.assign(prev, {
			[prop]: values[i]
		})
	}, {})
}

nc.recordArrayIndexLowerCase = function (arr, field, value, startIndex) {
	if (value == null) return -1
	let i = startIndex || 0
	if (field.indexOf('.') > 0) {
		while (i < arr.length) {
			const val = recData(arr[i], field)
			if (val == null) return -1
			if (val.toLowerCase() === value.toLowerCase()) {
				return i
			}
			i++
		}
	} else {
		while (i < arr.length) {
			const val = arr[i][field]
			if (val == null) return -1
			if (val.toLowerCase() === value.toLowerCase()) {
				return i
			}
			i++
		}
	}
	return -1
}

export function recordArrayIndex(arr, field, value, startIndex) {
	let i = startIndex || 0
	if (field.indexOf('.') > 0) {
		while (i < arr.length) {
			if (recData(arr[i], field) === value) {
				return i
			}
			i++
		}
	} else {
		while (i < arr.length) {
			if (arr[i] == null) {
				console.error('array index is null at position: ', i, arr)
			} else if (arr[i][field] === value) {
				return i
			}
			i++
		}
	}
	return -1
}
nc.recordArrayIndex = recordArrayIndex

export function recordArrayRecord(arr, field, value, startIndex) {
	const idx = recordArrayIndex(arr, field, value, startIndex)
	if (idx >= 0) {
		return arr[idx]
	}
}
nc.recordArrayRecord = recordArrayRecord

export function recordArrayField(arr, field, value, field2, startIndex) {
	const idx = recordArrayIndex(arr, field, value, startIndex)
	if (idx >= 0) {
		return arr[idx] && arr[idx][field2]
	}
}
nc.recordArrayField = recordArrayField

function clearDeepRec(item) {
	for (let key in item) {
		if (isObject(item[key])) {
			clearDeepRec(item[key])
		} else if (isArray(item[key])) {
			item[key] = []
		} else if (nc.isNumber(item[key])) {
			item[key] = 0
		} else {
			item[key] = ''
		}
	}
}
nc.clearDeepRec = clearDeepRec

function copyDeepRec(from, to, tablePrefix, keysToCopy, keysCopied) {
	if (from && to) {
		for (let key in from) {
			if (from[key] != null) {
				if (isObject(from[key])) {
					if (!isObject(to[key])) {
						to[key] = {}
					}
					copyDeepRec(from[key], to[key])
				} else {
					if (keysToCopy == null || keysToCopy[tablePrefix + '.' + key] != null) {
						if (to[key] !== from[key]) {
							if (keysCopied) {
								keysCopied.push(tablePrefix + '.' + key)
							}
							to[key] = from[key]
						}
					}
				}
			}
		}
	}
}
nc.copyDeepRec = copyDeepRec

nc.sortArray = function (arr, order) {
	// plain arr.sort() may be better because if compareFunction is omitted, the array is sorted according to each character's Unicode code point value, according to the string conversion of each element.
	if (order === 'desc') return arr.sort().reverse()
	return arr.sort()
	/* return arr.sort((a, b) => { // this works
		// console.log('a,b', a, b)
		if (a < b) {
			if (order === 'desc') return 1
			return -1
		}
		if (b > a) {
			if (order === 'desc') return -1
			return 1
		}
		return 0
	}) */
}

nc.isNumber = function (variable) {
	return !isNaN(parseFloat(variable)) && nc.isFinite(variable)
}

nc.cloneShallow = function (variable) {
	return Object.assign({}, variable)
}

nc.clone = function (variable, doNotCloneFields) {
	// return JSON.parse(JSON.stringify(variable))
	return clone(variable, doNotCloneFields)
}

export function sleep(ms) {
	return new Promise(resolve => setTimeout(resolve, ms))
}

export function debounce(func, wait, immediate) {
	// https://davidwalsh.name/javascript-debounce-function
	// Returns a function, that, as long as it continues to be invoked, will not be triggered.
	// The function will be called after it stops being called for N milliseconds.
	// If `immediate` is passed, trigger the function on the leading edge, instead of the trailing.
	let timeout
	return function () {
		let context = this
		let args = arguments
		let later = async function () {
			await sleep(1) // see: https://stackoverflow.com/questions/42218699/chrome-violation-violation-handler-took-83ms-of-runtime
			timeout = null
			if (!immediate) func.apply(context, args)
		}
		let callNow = immediate && !timeout
		clearTimeout(timeout)
		timeout = setTimeout(later, wait)
		if (callNow) func.apply(context, args)
	}
}
nc.debounce = debounce

function doNotCompareFields2(doNotCompareFields, fld) {
	return Object.keys(doNotCompareFields).reduce((acc, key) => {
		let key2 = peg.parseAfter(key, fld + '.')
		if (key2 !== key) {
			acc[key2] = true
		}
		return acc
	}, {})
}

nc.isEqual = function (a, b, doNotCompareFields) {
	// https://gist.github.com/hkdobrev/3169016, modified heavily, only isObject
	if (typeof a !== typeof b) {
		return false
	}
	if (isArray(a)) {
		if (a.length !== b.length) return false
		// If you don't care about the order of the elements inside
		// the array, you should sort both arrays here.
		for (let i = 0; i < a.length; ++i) {
			if (!nc.isEqual(a[i], b[i], doNotCompareFields)) {
				return false
			}
		}
	} else if (isObject(a)) {
		for (let fld in a) {
			if (doNotCompareFields == null || !doNotCompareFields[fld] || fld === 'json_data') {
				if (typeof b[fld] === 'undefined') {
					return false
				}
				if (b[fld] && !a[fld]) {
					return false
				}
				let t = typeof a[fld]
				let t2 = typeof b[fld]
				if (t === 'function' && (t2 === 'undefined' || a[fld].toString() !== b[fld].toString())) {
					return false
				}
				if (t === 'object' && t2 === 'object') {
					if (doNotCompareFields != null) {
						if (!nc.isEqual(a[fld], b[fld], doNotCompareFields2(doNotCompareFields, fld))) {
							return false
						}
					} else {
						if (!nc.isEqual(a[fld], b[fld])) {
							return false
						}
					}
				} else if (a[fld] != b[fld]) {
					return false
				}
			}
		}
		for (let fld in b) {
			if (doNotCompareFields == null || !doNotCompareFields[fld] || fld === 'json_data') {
				if (typeof a[fld] === 'undefined') {
					return false
				}
			}
		}
	} else {
		return a === b
	}
	return true
}
// nc.isEqual = isEqual

function isEqualLoose(a, b, doNotCompareFields, doNotCompareValue, failField, prefix, compareAmount) {
	prefix = prefix || ''
	// https://gist.github.com/hkdobrev/3169016, modified heavily, only isObject
	/* if (typeof a !== typeof b) {
		return false
	} */
	if (isArray(a) && isArray(b)) {
		debugger
		if (a.length !== b.length) return false
		// If you don't care about the order of the elements inside
		// the array, you should sort both arrays here.
		// this dows not work for objects unless they point to same object
		for (let i = 0; i < a.length; ++i) {
			if (a[i] != b[i]) {
				return false
			}
		}
	} else if (isObject(a) && isObject(b)) {
		for (let fld in a) {
			if (doNotCompareFields == null || !doNotCompareFields[fld] || fld === 'json_data') {
				if (a[fld] == null || b[fld] == null) {
					// null or undefined, loose == allow nulls in one side
				} else {
					let t = typeof a[fld]
					let t2 = typeof b[fld]
					if (t === 'function' && (t2 === 'undefined' || a[fld].toString() !== b[fld].toString())) {
						if (failField) {
							failField[prefix + fld] = { a: a[fld], b: b[fld] }
						}
						return false
					}
					if (t === 'object' && t2 === 'object') {
						const fields = doNotCompareFields2(doNotCompareFields, fld)
						if (!isEqualLoose(a[fld], b[fld], fields, doNotCompareValue, failField, fld + '.')) {
							return false
						}
					} else if (a[fld] != b[fld]) {
						let equal = false
						if (compareAmount && (typeof a[fld] === 'number' || typeof b[fld] === 'number')) {
							if (Math.abs(Number(a[fld]) - Number(b[fld])) < compareAmount) {
								equal = true
							}
						}
						if (!equal && (doNotCompareValue == null || (a[fld] != doNotCompareValue && b[fld] != doNotCompareValue))) {
							if (failField) {
								failField[prefix + fld] = { a: a[fld], b: b[fld] }
							}
							return false
						}
					}
				}
			}
		}
		for (let fld in b) {
			if (doNotCompareFields == null || !doNotCompareFields[fld] || fld === 'json_data') {
				if (a[fld] == null && typeof b[fld] !== 'object') {
					if (!a[fld]) {
						// "" or 0 are equal to nil or undefined
						if (failField) {
							failField[prefix + fld] = { a: a[fld], b: b[fld] }
						}
						return false
					}
				}
			}
		}
	} else if (doNotCompareValue != null) {
		if (a == b || a == doNotCompareValue || b == doNotCompareValue) {
			return true
		}
		if (compareAmount && (typeof a === 'number' || typeof b === 'number')) {
			if (Math.abs(Number(a) - Number(b)) < compareAmount) {
				return true
			}
		}
		failField[a] = { a: a, b: b }
		return false
	} else {
		if (a == b) {
			return true
		}
		if (compareAmount && (typeof a === 'number' || typeof b === 'number')) {
			if (Math.abs(Number(a) - Number(b)) < compareAmount) {
				return true
			}
		}
		failField[a] = { a: a, b: b }
		return false
	}
	return true
}
nc.isEqualLoose = isEqualLoose

nc.isNumeric = function (value) {
	return !isNaN(value)
}

const nativeIsFinite = isFinite
nc.isFinite = function (value) {
	return typeof value === 'number' && nativeIsFinite(value)
}

nc.type = function (item) {
	let type = Object.prototype.toString.call(item)
	let match = /(?!\[).+(?=\])/g
	type = type.match(match)[0].split(' ')[1]
	return type
}

nc.isString = function (variable) {
	return typeof variable === 'string' || variable instanceof String // https://stackoverflow.com/questions/4059147/check-if-a-variable-is-a-string-in-javascript
}

export function isDate(variable) {
	return variable instanceof Date // https://stackoverflow.com/questions/643782/how-to-check-whether-an-object-is-a-date
}
nc.isDate = isDate

export function isNil(variable) {
	return variable == null || typeof variable === 'undefined' // https://stackoverflow.com/questions/6003884/how-do-i-check-for-null-values-in-javascript
}
nc.isNil = isNil

nc.isFunction = function (variable) {
	return typeof variable === 'function' || variable instanceof Function
}

export function isArray(variable) {
	return variable && Array.isArray(variable) // https://stackoverflow.com/questions/4775722/check-if-object-is-array
}
nc.isArray = isArray

export function isObject(variable) {
	return variable === Object(variable) && !isArray(variable) // https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript
}
nc.isObject = isObject

function isEmptyObject(variable) {
	return Object.keys(variable).length === 0 && variable.constructor === Object // https://stackoverflow.com/questions/679915/how-do-i-test-for-an-empty-javascript-object
}
nc.isEmptyObject = isEmptyObject

nc.isEmpty = function (variable) {
	if (typeof variable !== 'object') {
		return true
	}
	if (isArray(variable) && variable.length === 0) {
		return false
	}
	return isEmptyObject(variable)
}

export function merge(target, ...sources) {
	// deep merge, copied from: https://stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge
	if (!sources.length) return target
	const source = sources.shift()
	if (isObject(target) && isObject(source)) {
		for (const key in source) {
			if (isObject(source[key])) {
				if (!target[key]) Object.assign(target, { [key]: {} })
				merge(target[key], source[key])
			} else {
				Object.assign(target, { [key]: source[key] })
			}
		}
	}
	return merge(target, ...sources)
}
nc.merge = merge

// see fromJson, toJson
export function fromJson(txt) {
	return JSON.parse(txt)
}
nc.fromJson = fromJson

export function toJson(data) {
	return JSON.stringify(data, null, 2)
}
nc.toJson = toJson

export function toJsonRaw(data) {
	return JSON.stringify(data)
}
nc.toJsonRaw = toJsonRaw

nc.replaceAll = function (txt, search, replacement) {
	return txt.split(search).join(replacement)
}

nc.htmlEscape = function (txt) {
	return txt.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;')
}

function sortFunction(sign) {
	return function (a, b) {
		if (a == null || b == null) {
			// may be null if all rows are not loaded
			return 0
		}
		if (a === void 0 || b === void 0) {
			// may be undefined if all rows are not loaded
			return 0
		}
		// result = a.localeCompare(b)
		if (typeof a === 'string') {
			a = a.toLowerCase()
		}
		if (typeof b === 'string') {
			b = b.toLowerCase()
		}
		if (a > b) {
			return sign // result =  1
		}
		if (a < b) {
			return -sign // result =  -1
		}
		return 0 // result * sign
	}
}

nc.dynamicSortMany = function (propertyArray) {
	const sortFunc = []
	const propertyName = []
	const propertyNeedRecData = []
	if (propertyArray.length % 2 !== 0) {
		console.error('sort definition must be even length', propertyArray)
	} else {
		let property, sortOrder
		let i = 0
		while (i < propertyArray.length) {
			property = propertyArray[i]
			sortOrder = propertyArray[i + 1]
			if (sortOrder === '>') {
				sortOrder = 1
			} else if (sortOrder === '<') {
				sortOrder = -1
			} else {
				console.error("sort order is not '<' or '>'", propertyArray)
				sortOrder = 1
			}
			sortFunc.push(sortFunction(sortOrder))
			propertyName.push(property)
			if (property.indexOf('.') >= 0) {
				propertyNeedRecData[propertyName.length - 1] = true
			} else {
				propertyNeedRecData[propertyName.length - 1] = false
			}
			i = i + 2
		}
	}
	return function (a, b) {
		// return sort function
		let ret
		let i = 0
		while (i < propertyName.length) {
			if (propertyNeedRecData[i]) {
				ret = sortFunc[i](recData(a, propertyName[i]), recData(b, propertyName[i]))
			} else {
				ret = sortFunc[i](a[propertyName[i]], b[propertyName[i]])
			}
			if (ret !== 0) {
				return ret
			}
			i = i + 1
		}
	}
}

nc.dynamicSort = function (property, order) {
	// http://stackoverflow.com/questions/1129216/sorting-objects-in-an-array-by-a-field-value-in-javascript
	if (isArray(property)) {
		return nc.dynamicSortMany(property)
	}
	return nc.dynamicSortMany([property, order])
}

function cleanUrl(url) {
	let pos
	pos = url.search('%3F')
	if (pos > 1) {
		console.warn('ma.cleanUrl: cutting all after %3F from ' + url)
		return url.substring(0, pos)
	}
	return url
}

nc.searchParameter = function (paramName) {
	let i, params, searchString, val
	searchString = window.location.search.substring(1) // http://www.w3schools.com/jsref/prop_loc_search.asp
	searchString = cleanUrl(searchString)
	i = null
	val = null
	params = searchString.split('&') // id=xxyyzz&name=asd&jj=kk
	i = 0
	while (i < params.length) {
		val = params[i].split('=') // id=xxyyzz
		if (val[0] === paramName) {
			return val[1]
		}
		i++
	}
	return '' // null
}
// nc.searchParameter = searchParameter
/* nc.removeNonObjectKeys = rec => {
	return Object.keys(rec).reduce((result, key) => {
		if (isObject(rec[key])) {
			// || isArray(rec[key])) {
			result[key] = rec[key]
		}
		return result
	}, {})
}

nc.copyRecByArrKeyValue = (rec, prefix, arr, arrKey, addCallback) => {
	return Object.keys(rec).reduce((result, key) => {
		if (isObject(rec[key])) {
			// result[key] = nc.copyRecByArrKeyValue(rec[key], prefix + key + '.', arr, arrKey, addCallback)
			result[key] = nc.copyRecByArrKeyValue(rec[key], '', arr, arrKey, addCallback)
		} else if (isArray(rec[key])) {
			result[key] = rec[key] // copy arrays as they are
		} else {
			const arrRec = nc.findRecFromArr(arr, arrKey, prefix + key)
			if (addCallback(arrRec)) {
				result[key] = rec[key]
			}
		}
		return result
	}, {})
} */

nc.findRecFromArr = function (arr, recordIdFldName, recordValue, defaultValue, resultArr) {
	if (recordValue === undefined) return null
	if (recordIdFldName.indexOf('.') < 0) {
		for (let rec of arr) {
			if (rec[recordIdFldName] === recordValue) {
				if (resultArr) {
					resultArr.push(rec)
				} else return rec
			}
		}
	} else {
		for (let rec of arr) {
			if (recData(rec, recordIdFldName) === recordValue) {
				if (resultArr) {
					resultArr.push(rec)
				} else return rec
			}
		}
	}
	if (defaultValue != null) {
		if (recordIdFldName.indexOf('.') < 0) {
			for (let rec of arr) {
				if (rec[recordIdFldName] === defaultValue) {
					if (resultArr) {
						resultArr.push(rec)
					} else return rec
				}
			}
		} else {
			for (let rec of arr) {
				if (recData(rec, recordIdFldName) === defaultValue) {
					if (resultArr) {
						resultArr.push(rec)
					} else return rec
				}
			}
		}
	}
	return null
}

export function recDataSet(rec, key, value, clearEmptySubKeyLevel) {
	if (key == null) {
		console.error(`nc.recDataSet() key is empty`)
		return null
	}
	if (rec == null) {
		console.error(`nc.recDataSet() target record is empty, key '${key}'`)
		return null
	}
	if (key.indexOf('.') === -1) {
		/* if (rec[key] == null) {
			console.debug(`  - update state key '${stateKey}.${key}' does not exist, value '${rec[key]}', new value: '${value}'`)
		} */
		rec[key] = value // vue 3
		return
	}
	const nameArr = key.split('.')
	let data = rec
	let parent
	let namePart = key
	for (let index = 0; index < nameArr.length; index++) {
		namePart = nameArr[index]
		/* 	if (namePart === 'json_data' && typeof value === 'string') {
			value = JSON.parse(value)
		} */
		if (index < nameArr.length - 1) {
			if (data[nameArr[index]] == null) {
				data[nameArr[index]] = {} // todo: this will not work as reactive, but it's ok with prev_rec
			}
			parent = data
			data = data[nameArr[index]]
		} else {
			let namePart = nameArr[index]
			if (data == null) {
				let pos = nameArr[index].indexOf('[')
				if (pos > 0) {
					namePart = nameArr[index].substring(0, pos)
					data = data[namePart]
					if (typeof data == 'object') {
						namePart = nameArr[index].substring(0, pos + 1)
						pos = namePart.indexOf(']')
						if (pos > 0) {
							namePart = namePart.substring(0, pos - 1)
							data = data[namePart]
						}
					}
				}
			}
			if (data != null) {
				data[namePart] = value
			}
		}
	}
	if (value === undefined) {
		delete data[namePart]
		if (clearEmptySubKeyLevel && nameArr.length > clearEmptySubKeyLevel) {
			if (nc.isEmptyObject(data)) {
				delete parent[nameArr[nameArr.length - 2]] // delete second last name part - is safe because we delete from parent, not orig rec
			}
		}
	} else {
		data[namePart] = value
	}
}
nc.recDataSet = recDataSet

export function recData(data, key) {
	if (data == null) {
		console.error(`nc.recData() data is empty, key '${key}'`)
		return null
	}
	if (key == null) {
		console.error(`nc.recData() key is empty, data:`, data)
		debugger
		return null
	}
	if (key.indexOf('.') === -1) {
		// && key.indexOf('[') === -1) // does not work in case of data[0]
		return data[key] // most of the cases, fast path
	}
	const nameArr = key.split('.')
	let namePart = key
	for (let index = 0; index < nameArr.length; index++) {
		namePart = nameArr[index]
		if (index < nameArr.length) {
			if (data[nameArr[index]] != null) {
				data = data[nameArr[index]] // nosemgrep
				if (index === nameArr.length - 1) {
					return data
				}
			} else {
				let pos = nameArr[index].indexOf('[')
				if (pos < 1) {
					return null
				} else {
					namePart = nameArr[index].substring(0, pos)
					data = data[namePart] // nosemgrep
					if (data == null) {
						return null
					}
					namePart = nameArr[index].substring(pos + 1)
					pos = namePart.indexOf(']')
					if (pos > 0) {
						namePart = namePart.substring(0, pos)
						if (isArray(data)) {
							pos = Number(namePart)
							if (data.length > pos - 1) {
								data = data[pos - 1] // nosemgrep, indexes are 1-based, js arrays are 0-based
								if (index === nameArr.length - 1) {
									return data // todo: this function does not handle multiple indexes like var[1][2] because of key.split('.')
								}
							} else {
								return null
							}
						} else {
							if (data[namePart]) {
								data = data[namePart] // nosemgrep
							} else {
								const quote = namePart.substring(0, 1) + namePart.substring(namePart.length - 1)
								if (quote === '""' || quote === "''") {
									namePart = namePart.substring(1, namePart.length - 1)
									data = data[namePart] // nosemgrep
									if (index === nameArr.length - 1) {
										return data
									}
								}
							}
						}
					}
				}
			}
		}
		if (data == null) {
			return null
		}
	}
	return data && data[namePart]
}
nc.recData = recData

export function recDataCopy(fromRec, fromKey, toRec, toKey, clearEmptySubKeyLevel) {
	const value = recData(fromRec, fromKey)
	if (value != null) {
		recDataSet(toRec, toKey, value, clearEmptySubKeyLevel)
	}
}
nc.recDataCopy = recDataCopy

nc.getRec = function (vm, key) {
	if (key == null) {
		console.error('getRec() key is empty')
		return
	}
	const state = vm.state || vm
	if (key.indexOf('.') === -1) {
		return state.rec && state.rec[key] // most of the cases, fast path
	}
	return state.rec && recData(state.rec, key)
}

nc.setRec = function (vm, recName, newValue, calledFrom, event) {
	const state = vm.state || vm
	state.commit(vm, 'rec', {
		key: recName,
		value: newValue,
		option: calledFrom,
		event: event
	})
}

nc.setPrevRec = function (vm, recName, calledFrom) {
	const state = vm.state || vm
	state.commit(vm, 'prev_rec', {
		key: recName,
		option: calledFrom
	})
	// this.state.prev_rec[this.recName] = nc.recData(this.state.rec, this.recName)
}
// use ui: function setMessage(vm, newValue, calledFrom)

nc.setHdr = function (vm, recName, newValue, calledFrom) {
	const state = vm.state || vm
	state.commit(vm, 'hdr', {
		key: recName,
		value: newValue,
		option: calledFrom
	})
}

nc.setArr = function (vm, recName, newValue, calledFrom) {
	const state = vm.state || vm
	state.commit(vm, 'arr', {
		key: recName,
		value: newValue,
		option: calledFrom
	})
}

nc.setGrid = function (vm, recName, newValue, calledFrom) {
	const state = vm.state || vm
	if (!newValue) {
		// const grid = vm.grid && vm.grid[recName]
		// if (!grid) return
		newValue = { data: [], column: [{}], info: {} }
	} else if (!newValue.data || newValue.data.error) {
		return
	}
	state.commit(vm, 'grid', {
		key: recName,
		value: newValue,
		option: calledFrom
	})
}

nc.setState = function (vm, keyName, newValue, calledFrom) {
	const state = vm.state || vm
	state.commit(vm, 'state', {
		key: keyName,
		value: newValue,
		option: calledFrom
	})
}

nc.mergeState = function (vm, keyName, newValue, calledFrom) {
	const state = vm.state || vm
	state.commit(vm, 'merge_state', {
		key: keyName,
		value: newValue,
		option: calledFrom
	})
}
// nc.setState = setState

nc.distinctValueArray = function (arr) {
	return arr.filter((item, index, array) => {
		return array.indexOf(item) === index
	})
}

nc.fieldKey = function (data, idxFld, multiple) {
	const idxTbl = {}
	if (multiple) {
		let val
		for (let item of data) {
			val = item[idxFld]
			if (!idxTbl[val]) {
				idxTbl[val] = []
			}
			idxTbl[val].push(item)
		}
	} else {
		for (let item of data) {
			idxTbl[item[idxFld]] = item
		}
	}
	return idxTbl
}

/*

nc.distinctRecValueArray = function (recArr, key) {
	let idx, j, keyArr, len1, rec, value, valueArr
	keyArr = {}
	valueArr = [] // in js array is different than in Lua
	idx = 0
	for (j = 0, len1 = recArr.length; j < len1; j++) {
		rec = recArr[j]
		value = rec[key]
		if (!keyArr[value]) {
			keyArr[value] = true
			valueArr[idx] = value
			idx = idx + 1 // in js indexes are zero-based
		}
	}
	return valueArr
}
 */

nc.isSafari = function () {
	return navigator.userAgent.indexOf('Safari') > -1 && navigator.userAgent.indexOf('Chrome') === -1
}

/* function isFirefox() {
	return navigator.userAgent.indexOf('Firefox') > -1
} */

nc.navigatorLanguage = function () {
	if (navigator.languages && navigator.languages.length) {
		return navigator.languages[0]
	} else {
		return navigator.language || navigator.browserLanguage || 'en'
	}
}

nc.decimalSeparator = function () {
	let language = nc.navigatorLanguage()
	if (nc.isSafari() && language.substr(0, 2) === 'en') {
		language = 'fi'
	}
	return (1.1).toLocaleString(language).substr(1, 1)
}

nc.isMac = function () {
	return window.navigator.platform.match('Mac') // navigator.appVersion.indexOf("Mac") !== -1
}

function iOSSafari() {
	let ua = window.navigator.userAgent
	let iOS = !!ua.match(/iPad/i) || !!ua.match(/iPhone/i)
	let webkit = !!ua.match(/WebKit/i)
	return iOS && webkit && !(ua.match(/CriOS/i) || ua.match(/FxiOS/i))
}

function iOSVersion() {
	if (iOSSafari()) {
		// supports iOS 2.0 and later: <http://bit.ly/TJjs1V>
		let v = navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/)
		return [parseInt(v[1], 10), parseInt(v[2], 10), parseInt(v[3] || 0, 10)][0]
	}
	return null
}

nc.iOSHeightFix = function (height, callback) {
	const ver = iOSVersion()
	if (ver && ver <= 10 && height.substr(height.length - 2) === 'vh') {
		const num = parseFloat(height.substr(0, height.length - 2))
		const landscape = Math.abs(window.orientation) === 90
		if (landscape) {
			height = num - 12 + 'vh' // remove 12vh that on landscape
		} else {
			height = num - 6 + 'vh' // remove 6vh that on portrait
		}
		if (callback) {
			window.addEventListener('orientationchange', function () {
				callback()
			})
		}
	}
	return height
}

nc.base64ToBinary = function (base64) {
	const raw = atob(base64)
	const rawLength = raw.length
	const array = new Uint8Array(new ArrayBuffer(rawLength))
	let i = 0
	while (i < rawLength) {
		array[i] = raw.charCodeAt(i)
		i++
	}
	return array
}

nc.base64toBlob = function (base64Data, contentType) {
	contentType = contentType || ''
	const sliceSize = 1024
	const byteCharacters = atob(base64Data.replace(/\s/g, ''))
	const bytesLength = byteCharacters.length
	const slicesCount = Math.ceil(bytesLength / sliceSize)
	const byteArrays = new Array(slicesCount)
	let sliceIndex = 0
	while (sliceIndex < slicesCount) {
		const begin = sliceIndex * sliceSize
		const end = Math.min(begin + sliceSize, bytesLength)
		const bytes = new Array(end - begin)
		let offset = begin
		let i = 0
		while (offset < end) {
			bytes[i] = byteCharacters[offset].charCodeAt(0)
			++i
			++offset
		}
		byteArrays[sliceIndex] = new Uint8Array(bytes)
		++sliceIndex
	}
	return new Blob(byteArrays, {
		type: contentType // , charset: 'utf-8'
	})
}

/*
// copied from: https://stackoverflow.com/questions/17184813/how-to-encode-decode-ascii85-in-javascript

function encodeAscii85(sSource) {
	let sSuffix, iStringLength, f
	let charArray = []
	if (!/[^\x00-\xFF]/.test(sSource)) {
		[sSource, sSuffix, iStringLength] = initForLoop(sSource)
		for (let iIndex = 0; iStringLength > iIndex; iIndex += 4) {
			f = (sSource.charCodeAt(iIndex) << 24) + (sSource.charCodeAt(iIndex + 1) << 16)
			f = f + (sSource.charCodeAt(iIndex + 2) << 8) + sSource.charCodeAt(iIndex + 3)
			appendNextChar(f, charArray)
		}
		(function truncate(oArray, b) {
			for (let m = b.length; m > 0; m--) oArray.pop()
		})(charArray, sSuffix)
		return "<~" + String.fromCharCode.apply(String, charArray) + "~>"
	} else {
		// Error Handling
	}

	function initForLoop(a) {
		let sSuffix = "\x00\x00\x00\x00".slice(a.length % 4 || 4)
		return [a += sSuffix, sSuffix, a.length]
	}

	function appendNextChar(f, oArray) {
		if (f === 0) {
			oArray.push(122)
		} else {
			let g, h, i, j, k
			k = f % 85, f = (f - k) / 85
			j = f % 85, f = (f - j) / 85
			i = f % 85, f = (f - i) / 85
			h = f % 85, f = (f - h) / 85
			g = f % 85
			oArray.push(g + 33, h + 33, i + 33, j + 33, k + 33)
		}
	}
}

function decodeAscii85(sSource) {
	let sSuffix, d, iStringLength
	let charArray = []
	if ("<~" === sSource.slice(0, 2) && "~>" === sSource.slice(-2)) {
		sSource = initForLoop(sSource)
		for (let iIndex = 0; iStringLength > iIndex; iIndex += 5) {
			let x = "charCodeAt"
			let w = 255
			d = 85 * 85 * 85 * 85 * (sSource[x](iIndex) - 33) + 85 * 85 * 85 * (sSource[x](iIndex + 1) - 33)
			d = d + 85 * 85 * (sSource[x](iIndex + 2) - 33) + 85 * (sSource[x](iIndex + 3) - 33) + (sSource[x](iIndex + 4) - 33)
			charArray.push(w & d >> 24, w & d >> 16, w & d >> 8, w & d)
		}
		(function truncate(oArray, b) {
			for (let m = b.length; m > 0; m--) oArray.pop()
		})(charArray, sSuffix)
		return String.fromCharCode.apply(String, charArray)
	} else {
		// Error Handling
	}

	function initForLoop(a) {
		let z = "replace",
			y = "slice"
		a = a[y](2, -2)[z](/\s/g, "")[z]("z", "!!!!!")
		sSuffix = "uuuuu"[y](a.length % 5 || 5)
		a += sSuffix
		iStringLength = a.length
		return a
	}

}
// */
