const {
  getDataType,
  isStorageStructGroup,
  normalizeGroup
} = require('../../domain/parameter-groups/model.js')
const {
  parseStructCatalog,
  parseStructDefinition: parseStructDefinitionSource
} = require('../../domain/parameter-groups/struct-parser.js')

function getRegisterByteLengthFromConfig(register) {
  const dataType = getDataType(register.dataType).key
  if (dataType === 'raw') {
    return Math.max(1, Number(register.sourceByteLength || register.byteLength || register.rawByteLength) || 1)
  }
  if (dataType === 'ascii' || dataType === 'utf8') return Math.max(1, Number(register.textByteLength) || 1)
  if (dataType === 'float' || dataType === 'int32_t' || dataType === 'uint32_t') return 4
  if (dataType === 'int8_t' || dataType === 'uint8_t') return 1

  return 2
}

function getRegistersByteLength(registers = []) {
  const explicitByteEnds = registers.map((register) => {
    const byteStart = Number(register && register.byteStart)
    if (!Number.isFinite(byteStart)) return null

    if (register.isBitField) {
      const bitOffset = Math.min(Math.max(Math.floor(Number(register.bitOffset) || 0), 0), 7)
      const bitWidth = Math.max(1, Math.round(Number(register.bitWidth) || 1))

      return Math.max(0, Math.floor(byteStart)) + Math.max(1, Math.ceil((bitOffset + bitWidth) / 8))
    }

    return Math.max(0, Math.floor(byteStart)) + getRegisterByteLengthFromConfig(register)
  }).filter((value) => Number.isFinite(value))

  if (explicitByteEnds.length) return Math.max.apply(null, explicitByteEnds)

  return registers.reduce((total, register) => total + getRegisterByteLengthFromConfig(register), 0)
}

function normalizeSymbolText(value) {
  return String(value || '')
    .replace(/^(?:IDATA|XDATA|DATA|CODE)[\s:_-]+/i, '')
    .replace(/^_+/, '')
    .replace(/[^A-Za-z0-9]/g, '')
    .toLowerCase()
}

function getStorageStructTypeName(group = {}) {
  return String(group.sourceSymbolType || group.sourceSymbolName || group.name || '')
}

function isStructCodeInfoEntry(group = {}) {
  const entryKind = String(group.sourceEntryKind || '').trim().toLowerCase()

  return !entryKind || entryKind === 'struct'
}

function isVariableCodeInfoEntry(group = {}) {
  return String(group.sourceEntryKind || '').trim().toLowerCase() === 'variable'
}

function structDefinitionNameMatches(group = {}, structInfo = {}) {
  const expectedName = normalizeSymbolText(getStorageStructTypeName(group))
  const structName = normalizeSymbolText(structInfo.name)

  return !!expectedName && !!structName && expectedName === structName
}

function findStructCompletion(group, catalog) {
  if (isStorageStructGroup(group) && isStructCodeInfoEntry(group)) {
    const matchedStruct = catalog.structs.find((structInfo) => structDefinitionNameMatches(group, structInfo))

    return matchedStruct
      ? {
        name: group.sourceSymbolName || group.name,
        registers: matchedStruct.registers,
        structName: matchedStruct.name
      }
      : null
  }

  const symbolName = group.sourceSymbolName || group.name
  const direct = catalog.variablesByName[normalizeSymbolText(symbolName)]
    || catalog.variablesByName[symbolName]

  if (direct) return direct

  const normalizedSymbol = normalizeSymbolText(symbolName)
  const normalizedType = normalizeSymbolText(group.sourceSymbolType)
  const expectedBytes = Number(group.sourceByteLength || group.byteLength || 0)

  const matchedStruct = catalog.structs.find((structInfo) => {
    const normalizedStructName = normalizeSymbolText(structInfo.name)
    if (normalizedType && normalizedType === normalizedStructName) {
      return true
    }
    if (normalizedSymbol && (
      normalizedSymbol === normalizedStructName
      || normalizedSymbol.indexOf(normalizedStructName) >= 0
      || normalizedStructName.indexOf(normalizedSymbol) >= 0
    )) {
      return true
    }

    return expectedBytes > 0 && getRegistersByteLength(structInfo.registers) === expectedBytes
  })

  return matchedStruct
    ? {
      name: symbolName,
      registers: matchedStruct.registers,
      structName: matchedStruct.name
    }
    : null
}

function getEnumLookupNames(group = {}) {
  const registers = Array.isArray(group.registers) ? group.registers : []
  const names = [
    group.sourceSymbolType,
    group.sourceSymbolName,
    group.name
  ]

  registers.forEach((register) => {
    names.push(register.sourceSymbolType, register.sourceSymbolName, register.name)
  })

  return names.map(normalizeSymbolText).filter(Boolean)
}

function findEnumCompletion(group, catalog = {}) {
  const enums = Array.isArray(catalog.enums) ? catalog.enums : []
  if (!isVariableCodeInfoEntry(group) || !enums.length) return null

  const names = getEnumLookupNames(group)
  if (!names.length) return null

  const enumVariablesByName = catalog.enumVariablesByName || {}
  for (const name of names) {
    const variableEnum = enumVariablesByName[name]
    if (variableEnum) return variableEnum
  }

  return enums.find((enumInfo) => (
    [enumInfo.name, enumInfo.typedefName, enumInfo.tagName]
      .concat(enumInfo.typeNames || [])
      .map(normalizeSymbolText)
      .filter(Boolean)
      .some((name) => names.indexOf(name) >= 0)
  )) || null
}

function getIntegerDataTypeForByteLength(byteLength) {
  const length = Number(byteLength)
  if (length === 1) return 'uint8_t'
  if (length === 2) return 'uint16_t'
  if (length === 4) return 'uint32_t'

  return ''
}

function cloneEnumOptions(enumInfo) {
  return (Array.isArray(enumInfo && enumInfo.options) ? enumInfo.options : []).map((option) => ({
    label: option.label || option.name,
    name: option.name || option.label,
    value: Number(option.value) || 0
  }))
}

function completeEnumVariableGroup(group, enumInfo) {
  if (!enumInfo) return group

  const enumOptions = cloneEnumOptions(enumInfo)
  if (!enumOptions.length) return group

  const sourceRegisters = Array.isArray(group.registers) ? group.registers : []
  const registers = sourceRegisters.map((register) => {
    const dataType = getIntegerDataTypeForByteLength(
      register.sourceByteLength || register.byteLength || group.sourceByteLength || group.byteLength
    )

    return dataType
      ? {
        ...register,
        dataType,
        enumName: enumInfo.name,
        enumOptions,
        sourceSymbolType: enumInfo.name || register.sourceSymbolType
      }
      : register
  })
  if (!registers.some((register, index) => register !== sourceRegisters[index])) return group

  return normalizeGroup({
    ...group,
    registers
  })
}

function createCompletedRegisters(group, completion) {
  const existingRemarksByByteStart = (Array.isArray(group.registers) ? group.registers : []).reduce((remarks, register) => {
    const byteStart = Number(register && register.byteStart)
    const remark = String(register && register.remark ? register.remark : '').trim()
    if (Number.isFinite(byteStart) && remark) remarks[Math.floor(byteStart)] = remark

    return remarks
  }, {})

  return completion.registers.map((register) => {
    const sourceAddress = (Number(group.sourceAddress) || Number(group.startAddress) || 0) + getRegisterByteStart(register)

    return {
      ...register,
      isStructField: true,
      remark: register.remark || existingRemarksByByteStart[Math.floor(Number(register.byteStart) || 0)] || '',
      sourceAddress,
      sourceAddressByteLength: group.sourceAddressByteLength,
      sourceAddressText: formatAddress(sourceAddress, group.sourceAddressWidth),
      sourceAddressWidth: group.sourceAddressWidth,
      sourceEntryKind: group.sourceEntryKind,
      sourceMemoryArea: group.sourceMemoryArea,
      sourceMemoryClass: group.sourceMemoryClass,
      sourceSymbolName: group.sourceSymbolName,
      sourceSymbolType: completion.structName || register.sourceSymbolType
    }
  })
}

function completeStructInstanceGroups(groups, sourceText, options = {}) {
  const catalog = parseStructCatalog(sourceText)
  let completedCount = 0
  let skippedCount = 0

  const nextGroups = groups.map((group) => {
    if (!group.sourceSymbolName || !group.sourceMemoryArea) return group
    if (isVariableCodeInfoEntry(group)) {
      const enumInfo = findEnumCompletion(group, catalog)
      if (!enumInfo) return group

      completedCount += 1
      return completeEnumVariableGroup(group, enumInfo)
    }
    if (!isStructCodeInfoEntry(group)) return group

    const completion = findStructCompletion(group, catalog)
    if (!completion || !completion.registers || !completion.registers.length) {
      skippedCount += 1
      return group
    }

    const expectedBytes = Number(group.sourceByteLength || group.byteLength || 0)
    const actualBytes = getRegistersByteLength(completion.registers)
    if (expectedBytes > 0 && actualBytes !== expectedBytes && options.strictLength !== false) {
      skippedCount += 1
      return group
    }

    completedCount += 1

    return normalizeGroup({
      ...group,
      layout: 'struct',
      quantity: completion.registers.length,
      registers: createCompletedRegisters(group, completion)
    })
  })

  return {
    completedCount,
    groups: nextGroups,
    skippedCount,
    structCount: catalog.structs.length,
    enumCount: Array.isArray(catalog.enums) ? catalog.enums.length : 0,
    variableCount: Object.keys(catalog.variablesByName).length
  }
}

function formatAddress(address, addressWidth) {
  const numberValue = Math.max(0, Math.floor(Number(address) || 0))
  const length = Number(addressWidth) === 32 || numberValue > 0xFFFF ? 8 : 4

  return `0x${numberValue.toString(16).toUpperCase().padStart(length, '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 || '')
  const addressKey = normalizeAddressKey(
    group.sourceAddress !== undefined ? group.sourceAddress : group.startAddress,
    group.sourceAddressText || group.startAddressText
  )
  if (area && symbolName && addressKey) return ['group', area, symbolName, addressKey].join('|')
  if (area && symbolName) return ['group', area, symbolName].join('|')

  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 structsMatchByLocation(existingGroup = {}, incomingGroup = {}) {
  const existingArea = normalizeDuplicateText(existingGroup.sourceMemoryArea || '')
  const incomingArea = normalizeDuplicateText(incomingGroup.sourceMemoryArea || '')
  const existingAddress = normalizeAddressKey(
    existingGroup.sourceAddress !== undefined ? existingGroup.sourceAddress : existingGroup.startAddress,
    existingGroup.sourceAddressText || existingGroup.startAddressText
  )
  const incomingAddress = normalizeAddressKey(
    incomingGroup.sourceAddress !== undefined ? incomingGroup.sourceAddress : incomingGroup.startAddress,
    incomingGroup.sourceAddressText || incomingGroup.startAddressText
  )

  if (existingArea && incomingArea && existingArea !== incomingArea) return false
  if (existingAddress && incomingAddress && existingAddress !== incomingAddress) return false

  return true
}

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)
    && structsMatchByLocation(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
  const sourceSymbolName = incomingGroup.sourceSymbolName || register.sourceSymbolName
  const sourceSymbolType = incomingGroup.sourceSymbolType || register.sourceSymbolType || sourceSymbolName

  return {
    ...register,
    rawBytes: [],
    rawValue: null,
    rawWords: [],
    sourceAddress,
    sourceAddressByteLength: incomingGroup.sourceAddressByteLength || register.sourceAddressByteLength,
    sourceAddressText: formatAddress(sourceAddress, incomingGroup.sourceAddressWidth || register.sourceAddressWidth),
    sourceAddressWidth: incomingGroup.sourceAddressWidth || register.sourceAddressWidth,
    sourceEntryKind: incomingGroup.sourceEntryKind,
    sourceMemoryArea: incomingGroup.sourceMemoryArea,
    sourceMemoryClass: incomingGroup.sourceMemoryClass,
    sourceSymbolName,
    sourceSymbolType
  }
}

function resolveMergedPollEnabled(existingGroup = {}, incomingGroup = {}, options = {}) {
  if (options.preserveExistingPollEnabled && existingGroup.pollEnabled === false) return false

  return incomingGroup.pollEnabled === false ? false : true
}

function mergePreservedStructGroupState(existingGroup, incomingGroup, options = {}) {
  const preservedRegisters = (Array.isArray(existingGroup.registers) ? existingGroup.registers : [])
    .map((register) => mergePreservedStructRegister(register, incomingGroup))

  return {
    ...incomingGroup,
    deleteVisible: false,
    expanded: existingGroup.expanded === true,
    id: existingGroup.id,
    pollEnabled: resolveMergedPollEnabled(existingGroup, incomingGroup, options),
    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, options)
  }

  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,
    pollEnabled: resolveMergedPollEnabled(existingGroup, incomingGroup, options),
    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
    }, options))
  } 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
}

function parseStructDefinition(sourceText) {
  return parseStructDefinitionSource(sourceText)
}

module.exports = {
  completeStructInstanceGroups,
  getRegistersByteLength,
  mergeImportedGroups,
  parseStructDefinition
}
