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
}
