const { normalizeGroup } = require('../../domain/parameter-groups/model.js') const { getRegistersByteLength } = require('./struct-completion.js') function formatAddress(address) { return `0x${Number(address || 0).toString(16).toUpperCase().padStart(4, '0')}` } function normalizeDuplicateText(value) { return String(value === undefined || value === null ? '' : value) .trim() .toLowerCase() } function normalizeStructMatchText(value) { return String(value === undefined || value === null ? '' : value) .trim() .replace(/^(?:IDATA|XDATA|DATA|CODE)[\s:_-]+/i, '') .replace(/^struct\s+/i, '') .replace(/\s+#\d+$/i, '') .replace(/^_+/, '') .replace(/[^A-Za-z0-9]/g, '') .toLowerCase() } function normalizeAddressKey(value, textValue) { const numberValue = Number(value) if (Number.isFinite(numberValue)) return String(Math.floor(numberValue)) return String(textValue === undefined || textValue === null ? '' : textValue) .trim() .toUpperCase() } function normalizeBitKey(source = {}) { const value = source.sourceBitOffset !== undefined && source.sourceBitOffset !== null && source.sourceBitOffset !== '' ? source.sourceBitOffset : source.bitOffset const numberValue = Number(value) return Number.isFinite(numberValue) ? String(Math.floor(numberValue)) : '' } function getRegisterDuplicateKey(register = {}, group = {}) { const area = normalizeDuplicateText(register.sourceMemoryArea || group.sourceMemoryArea || register.memoryArea || '') const symbolName = normalizeDuplicateText(register.sourceSymbolName || register.name || '') if (area && symbolName) return ['register', area, symbolName].join('|') const addressKey = normalizeAddressKey( register.sourceAddress !== undefined ? register.sourceAddress : register.address, register.sourceAddressText || register.addressText ) const bitKey = normalizeBitKey(register) if (!area && !symbolName && !addressKey) return '' return ['register', area, symbolName, addressKey, bitKey].join('|') } function isSingleRegisterAggregateGroup(group = {}) { const groupSymbolName = normalizeDuplicateText(group.sourceSymbolName || group.name || '') const registers = Array.isArray(group.registers) ? group.registers : [] return registers.some((register) => { const registerSymbolName = normalizeDuplicateText(register.sourceSymbolName || register.name || '') return registerSymbolName && groupSymbolName && registerSymbolName !== groupSymbolName }) } function getGroupDuplicateKey(group = {}) { const area = normalizeDuplicateText(group.sourceMemoryArea || '') const symbolName = normalizeDuplicateText(group.sourceSymbolName || group.name || '') if (area && symbolName) return ['group', area, symbolName].join('|') const addressKey = normalizeAddressKey( group.sourceAddress !== undefined ? group.sourceAddress : group.startAddress, group.sourceAddressText || group.startAddressText ) if (!area && !symbolName && !addressKey) return '' return ['group', area, symbolName, addressKey].join('|') } function getAggregateGroupDuplicateKey(source = {}) { const area = normalizeDuplicateText(source.sourceMemoryArea || source.memoryArea || '') const registerType = normalizeDuplicateText(source.registerType || '') const segment = normalizeDuplicateText(source.sourceSegment || '') return ['aggregate', area, registerType, segment].join('|') } function collectImportedVariableIndexes(groups = []) { return groups.reduce((indexes, group, groupIndex) => { if (!isSingleRegisterAggregateGroup(group)) { const groupKey = getGroupDuplicateKey(group) if (groupKey) indexes.groupIndexes[groupKey] = groupIndex } else { const aggregateKey = getAggregateGroupDuplicateKey(group) if (aggregateKey) indexes.aggregateGroupIndexes[aggregateKey] = groupIndex } ;(Array.isArray(group.registers) ? group.registers : []).forEach((register) => { const registerKey = getRegisterDuplicateKey(register, group) if (registerKey) { indexes.registerIndexes[registerKey] = { groupIndex, registerIndex: group.registers.indexOf(register) } } }) return indexes }, { aggregateGroupIndexes: {}, groupIndexes: {}, registerIndexes: {} }) } function getStructMatchNames(group = {}) { const registers = Array.isArray(group.registers) ? group.registers : [] const names = [ group.sourceSymbolName, group.sourceSymbolType, group.name, group.displayName ] registers.forEach((register) => { names.push(register.sourceSymbolType, register.sourceSymbolName) }) return names .map(normalizeStructMatchText) .filter(Boolean) .filter((name, index, list) => list.indexOf(name) === index) } function structsMatchByName(existingGroup = {}, incomingGroup = {}) { const existingNames = getStructMatchNames(existingGroup) const incomingNames = getStructMatchNames(incomingGroup) return existingNames.some((name) => incomingNames.indexOf(name) >= 0) } function getGroupByteLengthCandidates(group = {}) { const registers = Array.isArray(group.registers) ? group.registers : [] const candidates = [ group.sourceByteLength, group.byteLength, group.structByteLength, getRegistersByteLength(registers) ] registers.forEach((register) => { candidates.push(register.structByteLength) }) return candidates .map((value) => Number(value)) .filter((value) => Number.isFinite(value) && value > 0) .map((value) => Math.floor(value)) .filter((value, index, list) => list.indexOf(value) === index) } function structsMatchByByteLength(existingGroup = {}, incomingGroup = {}) { const existingLengths = getGroupByteLengthCandidates(existingGroup) const incomingLengths = getGroupByteLengthCandidates(incomingGroup) return existingLengths.some((length) => incomingLengths.indexOf(length) >= 0) } function isIncomingPlaceholderStructGroup(group = {}) { const registers = Array.isArray(group.registers) ? group.registers : [] return group.layout === 'struct' && registers.length > 0 && registers.every((register) => !!register.isPlaceholderByteField) } function hasImportedStructRegisters(group = {}) { const registers = Array.isArray(group.registers) ? group.registers : [] return group.layout === 'struct' && registers.length > 0 && registers.some((register) => !register.isPlaceholderByteField) } function canPreserveExistingStructLayout(existingGroup, incomingGroup, options = {}) { return options.preserveExistingStructLayout && hasImportedStructRegisters(existingGroup) && isIncomingPlaceholderStructGroup(incomingGroup) && structsMatchByName(existingGroup, incomingGroup) && structsMatchByByteLength(existingGroup, incomingGroup) } function getRegisterByteStart(register = {}) { const byteStart = Number(register.byteStart) return Number.isFinite(byteStart) ? Math.max(0, Math.floor(byteStart)) : 0 } function mergePreservedStructRegister(register = {}, incomingGroup = {}) { const byteStart = getRegisterByteStart(register) const sourceAddress = ((Number(incomingGroup.sourceAddress) || Number(incomingGroup.startAddress) || 0) + byteStart) & 0xFFFF const sourceSymbolName = incomingGroup.sourceSymbolName || register.sourceSymbolName const sourceSymbolType = incomingGroup.sourceSymbolType || register.sourceSymbolType || sourceSymbolName return { ...register, rawBytes: [], rawValue: null, rawWords: [], sourceAddress, sourceAddressText: formatAddress(sourceAddress), sourceMemoryArea: incomingGroup.sourceMemoryArea, sourceMemoryClass: incomingGroup.sourceMemoryClass, sourceSymbolName, sourceSymbolType } } function mergePreservedStructGroupState(existingGroup, incomingGroup) { const preservedRegisters = (Array.isArray(existingGroup.registers) ? existingGroup.registers : []) .map((register) => mergePreservedStructRegister(register, incomingGroup)) return { ...incomingGroup, deleteVisible: false, expanded: existingGroup.expanded === true, id: existingGroup.id, quantity: preservedRegisters.length, registers: preservedRegisters } } function findPreservableStructGroupIndex(groups = [], incomingGroup = {}, preferredIndex, options = {}) { if (preferredIndex !== undefined && canPreserveExistingStructLayout(groups[preferredIndex], incomingGroup, options)) { return preferredIndex } if (!options.preserveExistingStructLayout || !isIncomingPlaceholderStructGroup(incomingGroup)) return undefined return groups.findIndex((group, index) => ( index !== preferredIndex && canPreserveExistingStructLayout(group, incomingGroup, options) )) } function mergeImportedRegisterState(existingRegister, incomingRegister, options = {}) { if (!existingRegister) return incomingRegister const incomingRemark = incomingRegister.remark const shouldPreserveRemark = options.preserveExistingRemarks && !String(incomingRemark === undefined || incomingRemark === null ? '' : incomingRemark).trim() return { ...incomingRegister, id: existingRegister.id, inputValue: incomingRegister.inputValue !== undefined && incomingRegister.inputValue !== null ? incomingRegister.inputValue : existingRegister.inputValue, remark: shouldPreserveRemark ? existingRegister.remark : (incomingRemark !== undefined && incomingRemark !== null ? incomingRemark : existingRegister.remark), rawBytes: [], rawValue: null, rawWords: [] } } function mergeImportedGroupState(existingGroup, incomingGroup, options = {}) { if (!existingGroup) return incomingGroup if (canPreserveExistingStructLayout(existingGroup, incomingGroup, options)) { return mergePreservedStructGroupState(existingGroup, incomingGroup) } const existingRegisters = Array.isArray(existingGroup.registers) ? existingGroup.registers : [] const incomingRegisters = Array.isArray(incomingGroup.registers) ? incomingGroup.registers : [] return { ...incomingGroup, deleteVisible: false, expanded: existingGroup.expanded === true, id: existingGroup.id, registers: incomingRegisters.map((incomingRegister, index) => mergeImportedRegisterState( existingRegisters[index], incomingRegister, options )) } } function mergeAggregateImportedGroup(nextGroups, incomingGroup, indexes, options = {}) { const aggregateKey = getAggregateGroupDuplicateKey(incomingGroup) const aggregateGroupIndex = indexes.aggregateGroupIndexes[aggregateKey] let targetGroup = aggregateGroupIndex === undefined ? null : nextGroups[aggregateGroupIndex] let targetGroupIndex = aggregateGroupIndex let targetRegisters = targetGroup && Array.isArray(targetGroup.registers) ? targetGroup.registers.slice() : [] let addedRegisterCount = 0 let updatedRegisterCount = 0 ;(Array.isArray(incomingGroup.registers) ? incomingGroup.registers : []).forEach((incomingRegister) => { const registerKey = getRegisterDuplicateKey(incomingRegister, incomingGroup) const existingRef = registerKey ? indexes.registerIndexes[registerKey] : null if (existingRef) { const existingGroup = nextGroups[existingRef.groupIndex] const existingRegister = existingGroup && existingGroup.registers ? existingGroup.registers[existingRef.registerIndex] : null const mergedRegister = mergeImportedRegisterState(existingRegister, incomingRegister, options) if (targetGroupIndex !== undefined && existingRef.groupIndex === targetGroupIndex) { targetRegisters[existingRef.registerIndex] = mergedRegister } else if (existingGroup) { const registers = existingGroup.registers.slice() registers[existingRef.registerIndex] = mergedRegister nextGroups[existingRef.groupIndex] = normalizeGroup({ ...existingGroup, registers }) } updatedRegisterCount += 1 return } targetRegisters.push(incomingRegister) addedRegisterCount += 1 }) if (targetGroupIndex !== undefined && targetGroup) { nextGroups[targetGroupIndex] = normalizeGroup(mergeImportedGroupState(targetGroup, { ...incomingGroup, quantity: targetRegisters.length, registers: targetRegisters })) } else if (targetRegisters.length) { targetGroup = normalizeGroup({ ...incomingGroup, quantity: targetRegisters.length, registers: targetRegisters }) nextGroups.push(targetGroup) targetGroupIndex = nextGroups.length - 1 } return { addedGroupCount: targetGroupIndex === aggregateGroupIndex ? 0 : (targetRegisters.length ? 1 : 0), addedRegisterCount, updatedGroupCount: targetGroupIndex === aggregateGroupIndex && (addedRegisterCount || updatedRegisterCount) ? 1 : 0, updatedRegisterCount } } function mergeImportedGroups(existingGroups = [], incomingGroups = [], options = {}) { const nextGroups = existingGroups.slice() let indexes = collectImportedVariableIndexes(nextGroups) const result = { addedGroupCount: 0, addedRegisterCount: 0, groups: nextGroups, updatedGroupCount: 0, updatedRegisterCount: 0 } incomingGroups.forEach((incomingGroup) => { if (isSingleRegisterAggregateGroup(incomingGroup)) { const aggregateResult = mergeAggregateImportedGroup(nextGroups, incomingGroup, indexes, options) result.addedGroupCount += aggregateResult.addedGroupCount result.addedRegisterCount += aggregateResult.addedRegisterCount result.updatedGroupCount += aggregateResult.updatedGroupCount result.updatedRegisterCount += aggregateResult.updatedRegisterCount indexes = collectImportedVariableIndexes(nextGroups) return } const groupKey = getGroupDuplicateKey(incomingGroup) const existingGroupIndex = groupKey ? indexes.groupIndexes[groupKey] : undefined const preservableStructGroupIndex = findPreservableStructGroupIndex( nextGroups, incomingGroup, existingGroupIndex, options ) const targetGroupIndex = preservableStructGroupIndex >= 0 ? preservableStructGroupIndex : existingGroupIndex if (targetGroupIndex !== undefined) { const existingGroup = nextGroups[targetGroupIndex] nextGroups[targetGroupIndex] = normalizeGroup(mergeImportedGroupState(existingGroup, incomingGroup, options)) result.updatedGroupCount += 1 } else { nextGroups.push(incomingGroup) result.addedGroupCount += 1 } indexes = collectImportedVariableIndexes(nextGroups) }) result.changedCount = result.addedGroupCount + result.updatedGroupCount + result.addedRegisterCount + result.updatedRegisterCount return result } module.exports = { mergeImportedGroups }