const {
  clampInteger,
  createId,
  formatFixedValue,
  normalizeTextValue,
  padHex,
  pickFields
} = require('../../utils/base-utils.js')
const {
  BYTE_ADDRESS_MEMORY_AREAS,
  DATA_TYPE_OPTIONS,
  DEFAULT_DATA_TYPE,
  DEFAULT_REGISTER_TYPE,
  DEFAULT_TEXT_BYTE_LENGTH,
  GROUP_LAYOUT_REGISTER,
  GROUP_LAYOUT_STRUCT,
  MAX_MODBUS_ADDRESS,
  MAX_PARAMETER_GROUP_ITEMS,
  MAX_STORAGE_ADDRESS,
  REGISTER_TYPE_OPTIONS,
  SOURCE_GROUP_FIELDS,
  SOURCE_REGISTER_FIELDS,
  STRUCT_REGISTER_FIELDS
} = require('./constants.js')
const {
  evaluateValueFormula
} = require('./value-formula.js')
const {
  alignEvenByteLength,
  decodeRegisterValue,
  formatCoilDisplayValue,
  formatRawByteText,
  formatRawByteTextWithDefault,
  formatRawWordText,
  formatRegisterValue,
  getDataType,
  getDataTypeIndex,
  getRegisterByteLength,
  getRegisterValueTypeLabel,
  getRegisterWordCount,
  getRegisterWordCountAtOffset,
  isBitFieldRegister,
  isBitRegisterType,
  isByteRegister,
  isTextRegister,
  normalizeEnumOptions,
  normalizeBitOffset,
  normalizeBitWidth,
  normalizeTextByteLength,
  parseCoilValue,
  parseEnumValueText,
  parseNumberText,
  registerTypeIsBit,
  supportsRange,
  supportsUnit
} = require('./value-codec.js')
const {
  decodeRegisterFromByteCache,
  decodeRegisterFromWordCache,
  getGroupEncodedBytes,
  getGroupEncodedWords,
  getRegisterBytesFromByteCache,
  getRegisterEncodedBytes,
  getRegisterEncodedWords,
  getRegisterWordsFromByteCache,
  getRegisterWordsFromWordCache,
  getRegisterWriteValueText,
  splitWordSpans,
  validateRegisterValue
} = require('./register-io.js')

function normalizeAddress(value, fallback = 0) {
  if (typeof value === 'number') {
    return Number.isFinite(value) ? clampInteger(value, 0, MAX_MODBUS_ADDRESS, fallback) : fallback
  }

  const text = String(value === undefined || value === null ? '' : value).trim()
  if (!text) return fallback

  const hexText = text.toUpperCase().startsWith('0X') ? text.slice(2) : text
  if (/^[0-9A-F]+$/i.test(hexText)) {
    const parsedHex = parseInt(hexText, 16)
    return Number.isFinite(parsedHex) ? clampInteger(parsedHex, 0, MAX_MODBUS_ADDRESS, fallback) : fallback
  }

  const numberValue = Number(text)
  return Number.isFinite(numberValue) ? clampInteger(numberValue, 0, MAX_MODBUS_ADDRESS, fallback) : fallback
}

function normalizeStorageAddress(value, fallback = 0) {
  if (typeof value === 'number') {
    return Number.isFinite(value) ? clampInteger(value, 0, MAX_STORAGE_ADDRESS, fallback) : fallback
  }

  const text = String(value === undefined || value === null ? '' : value).trim()
  if (!text) return fallback

  const hexText = text.toUpperCase().startsWith('0X') ? text.slice(2) : text
  if (/^[0-9A-F]+$/i.test(hexText)) {
    const parsedHex = parseInt(hexText, 16)
    return Number.isFinite(parsedHex) ? clampInteger(parsedHex, 0, MAX_STORAGE_ADDRESS, fallback) : fallback
  }

  const numberValue = Number(text)
  return Number.isFinite(numberValue) ? clampInteger(numberValue, 0, MAX_STORAGE_ADDRESS, fallback) : fallback
}

function parseConfigAddress(value, options = {}) {
  const maxAddress = Number.isFinite(Number(options.maxAddress)) ? Number(options.maxAddress) : MAX_MODBUS_ADDRESS
  const maxHexLength = maxAddress > MAX_MODBUS_ADDRESS ? 8 : 4
  const label = options.label || '寄存器起始地址'
  if (typeof value === 'number') {
    return clampInteger(value, 0, maxAddress, 0)
  }

  const text = String(value === undefined || value === null ? '' : value).trim()
  const hexText = text.toUpperCase().startsWith('0X') ? text.slice(2) : text

  if (!new RegExp(`^[0-9A-F]{1,${maxHexLength}}$`, 'i').test(hexText)) {
    throw new Error(`${label}无效`)
  }

  return parseInt(hexText, 16)
}

function parseConfigQuantity(value, maxQuantity) {
  const text = String(value === undefined || value === null ? '' : value).trim()
  const quantity = Number(text)

  if (!Number.isInteger(quantity) || quantity < 1 || quantity > maxQuantity) {
    throw new Error(`寄存器数量需为 1 - ${maxQuantity}`)
  }

  return quantity
}

function getRegisterType(typeKey) {
  return REGISTER_TYPE_OPTIONS.find((item) => item.key === typeKey) || REGISTER_TYPE_OPTIONS[0]
}

function getRegisterTypeIndex(typeKey) {
  return Math.max(0, REGISTER_TYPE_OPTIONS.findIndex((item) => item.key === getRegisterType(typeKey).key))
}

function isStructLayout(layout) {
  return layout === GROUP_LAYOUT_STRUCT
}

function padWordHex(value) {
  const numberValue = Number(value || 0)
  const length = numberValue > MAX_MODBUS_ADDRESS ? 8 : 4

  return numberValue.toString(16).toUpperCase().padStart(length, '0')
}

function normalizeStorageAddressWidth(value, address = 0) {
  const numberValue = Number(value)
  if (numberValue === 16 || numberValue === 2) return 16
  if (numberValue === 32 || numberValue === 4) return 32

  return Number(address) > MAX_MODBUS_ADDRESS ? 32 : 16
}

function resolveStorageAddressWidth(source = {}, address = 0) {
  const codeInfoContext = source.codeInfoContext || {}
  const explicitWidth = source.sourceAddressWidth
    || source.storageAddressWidth
    || source.addressWidth
    || codeInfoContext.storageAddressWidth
    || codeInfoContext.addressWidth
    || codeInfoContext.codeInfoAddressWidth
  const explicitByteLength = source.sourceAddressByteLength
    || source.addressByteLength
    || codeInfoContext.sourceAddressByteLength
    || codeInfoContext.addressByteLength

  if (explicitWidth) return normalizeStorageAddressWidth(explicitWidth, address)
  if (explicitByteLength) return normalizeStorageAddressWidth(explicitByteLength, address)

  const memoryArea = String(source.sourceMemoryArea || '').trim().toUpperCase()
  if (memoryArea === 'ADDR32' || memoryArea === 'ADDRESS32') return 32

  return normalizeStorageAddressWidth(0, address)
}

function getStorageHexWidth(addressWidth, address = 0) {
  return normalizeStorageAddressWidth(addressWidth, address) === 16 ? 4 : 8
}

function getStorageAddressMax(addressWidth, address = 0) {
  return normalizeStorageAddressWidth(addressWidth, address) === 16
    ? MAX_MODBUS_ADDRESS
    : MAX_STORAGE_ADDRESS
}

function padStorageHex(value, addressWidth = 0) {
  const numberValue = Math.max(0, Math.floor(Number(value) || 0))

  return numberValue.toString(16).toUpperCase().padStart(getStorageHexWidth(addressWidth, numberValue), '0')
}

function formatAddressRange(startAddress, wordCount) {
  const address = normalizeAddress(startAddress, 0)
  const count = Math.max(1, Number(wordCount) || 1)
  const endAddress = address + count - 1
  const safeEndAddress = Math.min(endAddress, MAX_MODBUS_ADDRESS)
  const overflowText = endAddress > MAX_MODBUS_ADDRESS ? '+' : ''

  if (count <= 1) return `0x${padWordHex(address)}`

  return `0x${padWordHex(address)}-0x${padWordHex(safeEndAddress)}${overflowText}`
}

function formatStorageAddressRange(startAddress, byteCount, addressWidth = 0) {
  const address = normalizeStorageAddress(startAddress, 0)
  const count = Math.max(1, Number(byteCount) || 1)
  const addressMax = getStorageAddressMax(addressWidth, address)
  const endAddress = address + count - 1
  const safeEndAddress = Math.min(endAddress, addressMax)
  const overflowText = endAddress > addressMax ? '+' : ''

  if (count <= 1) return `0x${padStorageHex(address, addressWidth)}${overflowText}`

  return `0x${padStorageHex(address, addressWidth)}-0x${padStorageHex(safeEndAddress, addressWidth)}${overflowText}`
}

function isByteAddressedGroup(group = {}) {
  const memoryArea = String(group.sourceMemoryArea || '').trim().toUpperCase()

  return group.addressUnit === 'byte'
    || group.addressUnit === 'bytes'
    || BYTE_ADDRESS_MEMORY_AREAS.indexOf(memoryArea) >= 0
}

function formatRegisterAddressText(address, byteOffset, byteLength, registerType) {
  if (isBitRegisterType(registerType)) return `0x${padHex(address)}`
  if (byteLength === 1) return `0x${padHex(address)}${byteOffset === 0 ? 'H' : 'L'}`

  return `0x${padHex(address)}`
}

function formatBitFieldAddressText(address, byteOffset, bitOffset, bitWidth) {
  const byteText = formatRegisterAddressText(address, byteOffset, 1, DEFAULT_REGISTER_TYPE)
  const startBit = normalizeBitOffset(bitOffset)
  const endBit = startBit + normalizeBitWidth(bitWidth) - 1

  return endBit === startBit
    ? `${byteText}.b${startBit}`
    : `${byteText}.b${startBit}..${endBit}`
}

function isAddressRangeOverflow(startAddress, wordCount) {
  const address = normalizeAddress(startAddress, 0)
  const count = Math.max(1, Number(wordCount) || 1)

  return address + count - 1 > MAX_MODBUS_ADDRESS
}

function isStorageAddressRangeOverflow(startAddress, byteCount, addressWidth = 0) {
  const address = normalizeStorageAddress(startAddress, 0)
  const count = Math.max(1, Number(byteCount) || 1)
  const addressMax = getStorageAddressMax(addressWidth, address)

  return address + count - 1 > addressMax
}

function getMaxQuantity() {
  return MAX_PARAMETER_GROUP_ITEMS
}

function getRegisterSavedValue(register) {
  if (register.inputValue !== undefined && register.inputValue !== null) return normalizeTextValue(register.inputValue)
  if (register.value !== undefined && register.value !== null) return normalizeTextValue(register.value)

  return null
}

function normalizeRegisterDataType(register, registerType) {
  if (isBitRegisterType(registerType)) return DEFAULT_DATA_TYPE

  return getDataType(register.dataType || register.type || DEFAULT_DATA_TYPE).key
}

function createRegisterSourceMetaText(register) {
  const bitText = isBitFieldRegister(register)
    ? `bit${normalizeBitOffset(register.bitOffset)}:${normalizeBitWidth(register.bitWidth)}`
    : ''
  const sourceByteLength = Number(register.sourceByteLength)
  const sourceByteLengthText = Number.isFinite(sourceByteLength) && sourceByteLength > 0
    ? `${Math.floor(sourceByteLength)}B`
    : ''
  const parts = [
    register.sourceMemoryArea,
    register.sourceAddressText,
    sourceByteLengthText,
    bitText,
    register.sourceSymbolType && register.sourceSymbolType !== '---' ? register.sourceSymbolType : ''
  ].filter(Boolean)

  return parts.join(' · ')
}

function createGroupSourceMetaText(group) {
  const parts = [
    group.sourceMemoryArea,
    group.sourceAddressText,
    group.sourceSymbolName,
    group.sourceSegmentModule
  ].filter(Boolean)

  return parts.join(' · ')
}

function formatCompactNumber(value, precision = 4) {
  const text = formatFixedValue(value, precision)

  return text === '--' ? '--' : text.replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '')
}

function formatCardMetricValue(value) {
  if (value === '' || value === null || value === undefined) return ''

  return formatCompactNumber(value)
}

function readOptionalNumber(value) {
  if (value === '' || value === null || value === undefined) return null

  const numberValue = Number(value)

  return Number.isFinite(numberValue) ? numberValue : null
}

function addMetricItem(items, value, unit = '') {
  const text = formatCardMetricValue(value)
  if (!text) return

  items.push({
    text: `${text}${unit}`
  })
}

function assignOptionalNumber(target, key, value) {
  if (value === null || value === undefined) return

  target[key] = value
}

function normalizeMemoryEndian(value, fallback = 'big') {
  const text = String(value || '').trim().toLowerCase()
  if (text === 'little' || text === 'le' || text === '1') return 'little'
  if (text === 'big' || text === 'be' || text === '0') return 'big'

  return fallback
}

function normalizeStorageCodeInfoCard(codeInfo = null) {
  const hasCodeInfo = !!codeInfo && codeInfo.hasCodeInfo !== false
  const refVolt = hasCodeInfo ? readOptionalNumber(codeInfo.refVolt) : null
  const refVoltRaw = hasCodeInfo ? readOptionalNumber(codeInfo.refVoltRaw) : null
  const card = {
    alongDiv: hasCodeInfo ? readOptionalNumber(codeInfo.alongDiv) : null,
    ampGain: hasCodeInfo ? readOptionalNumber(codeInfo.ampGain) : null,
    busDiv: hasCodeInfo ? readOptionalNumber(codeInfo.busDiv) : null,
    caveFreq: hasCodeInfo ? readOptionalNumber(codeInfo.caveFreq) : null,
    chipModel: hasCodeInfo ? String(codeInfo.chipModel || '').trim() : '',
    maxPacketLength: hasCodeInfo ? (Number(codeInfo.maxPacketLength) || 0) : 0,
    memoryEndian: hasCodeInfo ? normalizeMemoryEndian(codeInfo.memoryEndian) : 'big',
    memoryEndianRaw: hasCodeInfo ? (Number(codeInfo.memoryEndianRaw) || 0) : 0,
    model: hasCodeInfo ? String(codeInfo.model || '').trim() : '',
    refVolt: hasCodeInfo
      ? (refVolt !== null ? refVolt : (refVoltRaw !== null ? refVoltRaw / 10 : null))
      : null,
    refVoltRaw: refVoltRaw === null ? 0 : refVoltRaw,
    rsShunt: hasCodeInfo ? readOptionalNumber(codeInfo.rsShunt) : null,
    addressWidth: hasCodeInfo
      ? (Number(codeInfo.addressWidth || codeInfo.codeInfoAddressWidth || codeInfo.storageAddressWidth) || 0)
      : 0
  }
  const codeInfoContext = hasCodeInfo
    ? {
      addressWidth: card.addressWidth,
      codeInfoAddressWidth: card.addressWidth,
      maxPacketLength: card.maxPacketLength,
      memoryEndian: card.memoryEndian,
      memoryEndianRaw: card.memoryEndianRaw,
      storageAddressWidth: card.addressWidth
    }
    : {}
  if (hasCodeInfo) {
    assignOptionalNumber(codeInfoContext, 'alongDiv', card.alongDiv)
    assignOptionalNumber(codeInfoContext, 'ampGain', card.ampGain)
    assignOptionalNumber(codeInfoContext, 'busDiv', card.busDiv)
    assignOptionalNumber(codeInfoContext, 'caveFreq', card.caveFreq)
    assignOptionalNumber(codeInfoContext, 'refVolt', card.refVolt)
    assignOptionalNumber(codeInfoContext, 'rsShunt', card.rsShunt)
  }
  const metricItems = []
  addMetricItem(metricItems, card.caveFreq, 'KHz')
  addMetricItem(metricItems, card.refVolt, 'V')
  addMetricItem(metricItems, card.ampGain)
  addMetricItem(metricItems, card.rsShunt, 'mΩ')

  return {
    ...card,
    codeInfoContext,
    hasCodeInfo,
    metricItems
  }
}

function isStorageStructGroup(group = {}) {
  return isStructLayout(group.layout)
    && isByteAddressedGroup(group)
    && !!String(group.sourceMemoryArea || '').trim()
}

function isStorageMemoryGroup(group = {}) {
  return isByteAddressedGroup(group)
    && !!String(group.sourceMemoryArea || '').trim()
}

function isStorageScalarEntryKind(entryKind) {
  const text = String(entryKind || '').trim().toLowerCase()

  return text === 'enum' || text === 'variable'
}

function isStorageScalarGroup(group = {}) {
  return isStorageMemoryGroup(group)
    && !isStructLayout(group.layout)
    && isStorageScalarEntryKind(group.sourceEntryKind)
}

function isStorageSourceLockedGroup(group = {}) {
  return isStorageMemoryGroup(group)
}

function isStorageSourceLockedRegister(group = {}, register = {}) {
  return isStorageSourceLockedGroup(group) || isStorageMemoryGroup(register)
}

function isParameterGroupPollEnabled(group = {}) {
  return group.pollEnabled !== false
}

function getStorageAreaText(group = {}, lowercase = false) {
  const area = String(group.sourceMemoryArea || '').trim()
  return lowercase ? area.toLowerCase() : area.toUpperCase()
}

function stripStorageAreaPrefix(text) {
  const source = String(text || '').trim()
  const cleaned = source.replace(/^(?:IDATA|XDATA|DATA|CODE)[\s:_-]+/i, '').trim()

  return cleaned || source
}

function formatRegisterStartAddressText(address) {
  return `0x${padWordHex(address)}`
}

function formatStorageStartAddressText(address, addressWidth = 0) {
  return `0x${padStorageHex(address, addressWidth)}`
}

function formatStartAddressText(address, maxAddress = MAX_MODBUS_ADDRESS) {
  return `0x${padHex(address, maxAddress > MAX_MODBUS_ADDRESS ? 8 : 4)}`
}

function formatStorageHexBytes(bytes = [], byteLength = 1) {
  const safeLength = Math.max(1, Math.floor(Number(byteLength) || 1))
  const source = Array.isArray(bytes) ? bytes : []

  return `0x${Array.from({ length: safeLength }, (_, index) => (
    (Number(source[index] || 0) & 0xFF).toString(16).toUpperCase().padStart(2, '0')
  )).join('')}`
}

function formatStorageRegisterHexValue(register, rawBytes = []) {
  if (isTextRegister(register.dataType)) {
    return formatRawByteTextWithDefault(rawBytes, register.byteLength)
  }

  return formatStorageHexBytes(rawBytes, register.byteLength)
}

function getStorageRegisterTypeText(register = {}) {
  const enumType = String(register.enumName || '').trim()
  if (enumType) return enumType

  const entryKind = String(register.sourceEntryKind || '').trim().toLowerCase()
  const sourceValueType = String(register.sourceValueType || register.sourceElementType || '').trim().toLowerCase()
  const isAggregateField = entryKind === 'struct' || sourceValueType === 'struct'
  const valueTypeCandidates = [
    register.dataTypeText,
    register.dataType
  ]
  const sourceTypeCandidates = [
    register.sourceDefinitionName,
    register.sourceSymbolType,
    register.sourceValueType,
    register.sourceElementType
  ]
  const candidates = isAggregateField
    ? valueTypeCandidates.concat(sourceTypeCandidates)
    : sourceTypeCandidates.concat(valueTypeCandidates)

  for (const value of candidates) {
    const text = String(value || '').trim()
    if (text && text !== '---') return text
  }

  return ''
}

function findEnumOptionByValue(register = {}) {
  const rawValue = Number(register.rawValue)
  if (!Number.isFinite(rawValue)) return null

  return normalizeEnumOptions(register).find((option) => (
    Number(option && option.value) === rawValue
  )) || null
}

function findEnumOptionByInputValue(register = {}) {
  const valueText = normalizeTextValue(register.inputValue).trim()
  if (!valueText || valueText === '--') return null

  let parsedValue = parseEnumValueText(register, valueText)
  if (parsedValue === null) parsedValue = parseNumberText(valueText, register.dataType)
  if (parsedValue === null) return null

  return normalizeEnumOptions(register).find((option) => (
    Number(option.value) === Number(parsedValue)
  )) || null
}

function findScalarEnumOption(register = {}) {
  if (register.isDirty) return findEnumOptionByInputValue(register)
  if (!hasReadScalarValue(register)) return null

  return findEnumOptionByValue(register)
}

function hasReadScalarValue(register = {}) {
  return register.rawValue !== null
    && register.rawValue !== undefined
    && !Array.isArray(register.rawValue)
}

function isEnumSourceText(value) {
  const text = String(value || '').trim().toLowerCase()

  return text === 'enum' || /^enum\b/.test(text) || /\benum\b/.test(text)
}

function isEnumLikeRegister(group = {}, register = {}) {
  if (String(register.enumName || '').trim()) return true
  if (normalizeEnumOptions(register).length) return true

  return [
    group.sourceEntryKind,
    group.sourceElementType,
    group.sourceValueType,
    group.sourceSymbolType,
    register.sourceEntryKind,
    register.sourceElementType,
    register.sourceValueType,
    register.sourceSymbolType
  ].some(isEnumSourceText)
}

function formatScalarDecimalValue(register = {}) {
  const rawValue = register.rawValue
  if (rawValue === null || rawValue === undefined || Array.isArray(rawValue)) return ''

  const enumOption = findEnumOptionByValue(register)
  if (enumOption) return `${enumOption.label || enumOption.name} (${Number(rawValue)})`

  const dataType = getDataType(register.dataType).key
  if (/^(?:u?int)(?:64|128|256)_t$/.test(dataType)) {
    return normalizeTextValue(rawValue || register.displayValue || '--') || '--'
  }

  const numberValue = Number(rawValue)
  if (!Number.isFinite(numberValue)) return normalizeTextValue(register.displayValue || '--') || '--'

  return (dataType === 'float' || dataType === 'double')
    ? formatCompactNumber(numberValue, 6)
    : String(Math.trunc(numberValue))
}

function formatScalarInputValue(register = {}) {
  const dirtyInputValue = normalizeTextValue(register.inputValue).trim()
  if (register.isDirty) return dirtyInputValue
  if (!hasReadScalarValue(register)) return ''

  const rawValue = register.rawValue
  if (typeof rawValue === 'bigint') return rawValue.toString()
  if (typeof rawValue === 'number') {
    if (!Number.isFinite(rawValue)) return ''

    return Number.isInteger(rawValue) ? String(rawValue) : formatCompactNumber(rawValue, 6)
  }

  return normalizeTextValue(rawValue).trim()
}

function createScalarDefinitionText(register = {}, group = {}) {
  const enumOption = findEnumOptionByValue(register)
  const typeName = String(register.enumName || register.sourceSymbolType || group.sourceSymbolType || '').trim()
  const dataTypeText = register.dataType === 'raw' ? '' : (register.dataTypeText || register.dataType)
  const definitionText = typeName || dataTypeText

  if (enumOption && definitionText) return `${definitionText}:${enumOption.label || enumOption.name}`
  if (enumOption) return enumOption.label || enumOption.name

  return definitionText
}

function createScalarEnumDefinitionText(register = {}, group = {}) {
  if (!normalizeEnumOptions(register).length) return ''

  const enumOption = findScalarEnumOption(register)
  if (!enumOption) return ''

  return enumOption.label || enumOption.name
}

function normalizeRegister(register, group, index, address, byteOffset = 0) {
  const registerType = getRegisterType(group.registerType).key
  const layout = isStructLayout(group.layout) ? GROUP_LAYOUT_STRUCT : GROUP_LAYOUT_REGISTER
  const isStorageMemory = isStorageMemoryGroup(group)
  const sourceMetadataLocked = isStorageSourceLockedRegister(group, register)
  const byteAddressed = isByteAddressedGroup(group)
  const dataType = normalizeRegisterDataType(register, registerType)
  const bitOffset = normalizeBitOffset(register.bitOffset)
  const bitWidth = normalizeBitWidth(register.bitWidth)
  const isBitField = !isBitRegisterType(registerType) && isBitFieldRegister(register)
  const isPlaceholderByteField = !!register.isPlaceholderByteField
  const textByteLength = isTextRegister(dataType)
    ? normalizeTextByteLength(register.textByteLength, DEFAULT_TEXT_BYTE_LENGTH)
    : ''
  const defaultValue = normalizeTextValue(register.defaultValue)
  const savedValue = getRegisterSavedValue(register)
  const inputValue = savedValue === null ? defaultValue : savedValue
  const rawValue = register.rawValue === undefined ? null : register.rawValue
  const memoryEndian = isStorageMemory
    ? normalizeMemoryEndian(register.memoryEndian || group.codeInfoContext && group.codeInfoContext.memoryEndian)
    : 'big'
  const storageAddressWidth = isStorageMemory
    ? resolveStorageAddressWidth({
      ...group,
      sourceAddressByteLength: register.sourceAddressByteLength || group.sourceAddressByteLength,
      sourceAddressWidth: register.sourceAddressWidth || group.sourceAddressWidth,
      sourceMemoryArea: register.sourceMemoryArea || group.sourceMemoryArea
    }, address)
    : 0
  const byteLength = isBitRegisterType(registerType)
    ? 1
    : getRegisterByteLength(dataType, { ...register, bitOffset, bitWidth, isBitField, layout, textByteLength })
  const registerCount = isBitRegisterType(registerType)
    ? 1
    : getRegisterWordCountAtOffset(dataType, byteOffset, { ...register, bitOffset, bitWidth, isBitField, layout, textByteLength })
  const canShowUnit = !isBitRegisterType(registerType) && !isBitField && supportsUnit(dataType)
  const rawWords = Array.isArray(register.rawWords)
    ? register.rawWords.slice(0, registerCount).map((word) => Number(word) & 0xFFFF)
    : []
  const rawBytes = Array.isArray(register.rawBytes)
    ? register.rawBytes.slice(0, byteLength).map((byte) => Number(byte) & 0xFF)
    : []
  const defaultRawValueText = rawValue === null
    ? '--'
    : (isBitRegisterType(registerType)
      ? formatCoilDisplayValue(rawValue)
      : (byteAddressed ? formatRawByteText(rawBytes) : formatRawWordText(rawWords)))
  const hasRawValue = rawValue !== null && rawValue !== undefined
  const rawValueText = isStorageMemory
    ? (hasRawValue ? formatStorageRegisterHexValue({
      ...register,
      byteLength,
      dataType
    }, rawBytes) : '')
    : defaultRawValueText
  const displayValue = rawValue === null
    ? (inputValue.trim() ? inputValue : '--')
    : formatRegisterValue({ ...register, dataType, byteOffset, memoryEndian }, rawValue)
  const conversionFormula = normalizeTextValue(register.conversionFormula).trim()
  const conversionResult = conversionFormula && rawValue !== null
    ? evaluateValueFormula(conversionFormula, rawValue, group.codeInfoContext || {})
    : null
  const convertedDisplayValue = conversionResult && conversionResult.ok
    ? conversionResult.text
    : displayValue
  const displayMetaText = conversionResult && conversionResult.ok
    ? `raw ${displayValue}`
    : ''
  const addressRangeText = isBitRegisterType(registerType)
    ? `0x${padHex(address)}`
    : (byteAddressed
      ? (isStorageMemory
        ? formatStorageAddressRange(address, Math.max(1, byteLength), storageAddressWidth)
        : formatAddressRange(address, Math.max(1, byteLength)))
      : formatAddressRange(address, registerCount))
  const addressText = isBitField
    ? (byteAddressed
      ? `${isStorageMemory
        ? formatStorageAddressRange(address, Math.max(1, byteLength), storageAddressWidth)
        : formatAddressRange(address, Math.max(1, byteLength))}.b${bitOffset}${bitWidth > 1 ? `..b${bitOffset + bitWidth - 1}` : ''}`
      : formatBitFieldAddressText(address, byteOffset, bitOffset, bitWidth))
    : (byteAddressed
      ? (isStorageMemory
        ? formatStorageAddressRange(address, Math.max(1, byteLength), storageAddressWidth)
        : formatAddressRange(address, Math.max(1, byteLength)))
      : formatRegisterAddressText(address, byteOffset, byteLength, registerType))
  const registerStartAddressText = isStorageMemory
    ? formatStorageStartAddressText(address, storageAddressWidth)
    : formatRegisterStartAddressText(address)
  const registerTypeText = getStorageRegisterTypeText({
    ...register,
    dataType,
    dataTypeText: getRegisterValueTypeLabel(dataType)
  })
  const metaText = isStorageMemory
    ? [registerStartAddressText, registerTypeText, rawValueText].filter(Boolean).join(' ')
    : `${addressText} ${rawValueText}`.trim()
  const cardMetaText = isStorageMemory
    ? metaText
    : (sourceMetadataLocked ? '' : metaText)
  const name = register.name || `寄存器 ${index + 1}`

  return {
    address,
    addressRangeText,
    addressText,
    bitOffset: isBitField ? bitOffset : '',
    bitWidth: isBitField ? bitWidth : '',
    byteLength,
    byteLengthText: isBitRegisterType(registerType)
      ? '1bit'
      : (isBitField
        ? (bitWidth === 1 ? `1bit/占${byteLength}B` : `${bitWidth}bit/占${byteLength}B`)
        : (textByteLength && textByteLength !== byteLength ? `${textByteLength}B/占${byteLength}B` : `${byteLength}B`)),
    byteStart: Math.max(0, Math.floor(Number(register.byteStart) || 0)),
    dataType,
    dataTypeIndex: getDataTypeIndex(dataType),
    dataTypeText: getRegisterValueTypeLabel(dataType),
    defaultValue,
    displayMetaText,
    displayValue: convertedDisplayValue,
    id: register.id || createId('gm-reg'),
    inputType: isTextRegister(dataType) ? 'text' : 'text',
    inputValue,
    isStructField: layout === GROUP_LAYOUT_STRUCT || !!register.isStructField,
    layout,
    isBitField,
    isPlaceholderByteField,
    isDirty: !!register.isDirty,
    maxValue: normalizeTextValue(register.maxValue),
    displayName: name,
    metaText: cardMetaText,
    minValue: normalizeTextValue(register.minValue),
    memoryEndian,
    name,
    conversionFormula,
    conversionFormulaErrorText: conversionResult && conversionResult.errorText ? conversionResult.errorText : '',
    rawValue,
    rawValueText,
    rawBytes,
    rawWords,
    registerCount,
    registerStartAddressText,
    registerTypeText,
    byteOffset,
    registerType,
    showDataType: !isBitRegisterType(registerType),
    showRange: !isBitRegisterType(registerType) && !isBitField && supportsRange(dataType),
    showTextLength: !isBitRegisterType(registerType) && isTextRegister(dataType),
    showUnit: canShowUnit,
    textByteLength,
    unit: canShowUnit ? normalizeTextValue(register.unit).trim() : '',
    structByteLength: register.structByteLength,
    remark: register.remark || '',
    ...pickFields(register, SOURCE_REGISTER_FIELDS),
    sourceMetaText: createRegisterSourceMetaText(register),
    sourceMetadataLocked
  }
}

function normalizeGroup(group) {
  const registerType = getRegisterType(group.registerType || group.type || DEFAULT_REGISTER_TYPE)
  const sourceMemoryArea = String(group.sourceMemoryArea || '').trim()
  const isStorageGroupSource = !!sourceMemoryArea || group.addressUnit === 'byte' || group.addressUnit === 'bytes'
  const startAddress = isStorageGroupSource
    ? normalizeStorageAddress(
      group.sourceAddress !== undefined && group.sourceAddress !== null && group.sourceAddress !== ''
        ? group.sourceAddress
        : group.startAddress,
      0
    )
    : normalizeAddress(group.startAddress, 0)
  const maxQuantity = getMaxQuantity(registerType.key)
  const sourceRegisters = Array.isArray(group.registers) ? group.registers : []
  const codeInfoContext = group.codeInfoContext || {}
  const layout = isStructLayout(group.layout) ? GROUP_LAYOUT_STRUCT : GROUP_LAYOUT_REGISTER
  const byteAddressed = isByteAddressedGroup(group)
  const hasExplicitQuantity = group.quantity !== undefined && group.quantity !== null && group.quantity !== ''
  const quantity = hasExplicitQuantity
    ? clampInteger(group.quantity, 1, maxQuantity, 1)
    : clampInteger(sourceRegisters.length || 1, 1, maxQuantity, 1)
  const baseGroup = {
    deleteVisible: !!group.deleteVisible,
    expanded: group.expanded === true,
    id: group.id || createId('gm-group'),
    isStructLayout: layout === GROUP_LAYOUT_STRUCT,
    layout,
    name: String(group.name || group.groupName || '寄存器组').trim() || '寄存器组',
    pollEnabled: group.pollEnabled === false ? false : true,
    quantity,
    registerType: registerType.key,
    startAddress,
    touchStartX: 0,
    codeInfoContext,
    ...pickFields(group, SOURCE_GROUP_FIELDS)
  }
  const registers = []
  let nextAddress = startAddress
  let nextByteOffset = 0

  for (let index = 0; index < quantity; index += 1) {
    const sourceRegister = sourceRegisters[index] || {}
    let normalizedSourceRegister = sourceRegister
    const dataType = normalizeRegisterDataType(sourceRegister, baseGroup.registerType)
    const textByteLength = isTextRegister(dataType)
      ? normalizeTextByteLength(sourceRegister.textByteLength, DEFAULT_TEXT_BYTE_LENGTH)
      : ''
    const isBitRegister = isBitRegisterType(baseGroup.registerType)
    let address = startAddress + index
    let byteOffset = 0

    if (!isBitRegister) {
      const explicitByteStart = Number(sourceRegister.byteStart)
      const hasExplicitByteStart = layout === GROUP_LAYOUT_STRUCT && Number.isFinite(explicitByteStart)
      const byteLength = getRegisterByteLength(dataType, { ...sourceRegister, layout, textByteLength })
      if (layout !== GROUP_LAYOUT_STRUCT && !isByteRegister(dataType) && nextByteOffset % 2 !== 0) {
        nextByteOffset += 1
      }

      const currentByteStart = hasExplicitByteStart
        ? Math.max(0, Math.floor(explicitByteStart))
        : nextByteOffset

      address = byteAddressed ? startAddress + currentByteStart : startAddress + Math.floor(currentByteStart / 2)
      byteOffset = byteAddressed ? 0 : currentByteStart % 2
      normalizedSourceRegister = {
        ...sourceRegister,
        byteStart: currentByteStart
      }
      nextByteOffset = Math.max(nextByteOffset, currentByteStart + byteLength)
    }

    const register = normalizeRegister(normalizedSourceRegister, baseGroup, index, address, byteOffset)
    registers.push(register)
    if (isBitRegister) nextAddress += register.registerCount
  }

  const byteLength = isBitRegisterType(baseGroup.registerType)
    ? Math.max(1, nextAddress - startAddress)
    : Math.max(1, nextByteOffset)
  const paddedByteLength = isBitRegisterType(baseGroup.registerType)
    ? byteLength
    : (byteAddressed ? byteLength : alignEvenByteLength(byteLength))
  const wordQuantity = isBitRegisterType(baseGroup.registerType)
    ? Math.max(1, nextAddress - startAddress)
    : (byteAddressed ? Math.max(1, byteLength) : Math.max(1, paddedByteLength / 2))
  const addressSpan = byteAddressed ? Math.max(1, byteLength) : wordQuantity
  const storageMemory = isStorageMemoryGroup(baseGroup)
  const storageScalarGroup = isStorageScalarGroup(baseGroup)
  const sourceMetadataLocked = isStorageSourceLockedGroup(baseGroup)
  const storageAddressWidth = storageMemory ? resolveStorageAddressWidth(baseGroup, startAddress) : 0
  const storageAddressMax = getStorageAddressMax(storageAddressWidth, startAddress)
  const addressOverflow = storageMemory
    ? isStorageAddressRangeOverflow(startAddress, addressSpan, storageAddressWidth)
    : isAddressRangeOverflow(startAddress, addressSpan)
  const endAddress = startAddress + addressSpan - 1
  const addressMax = storageMemory ? storageAddressMax : MAX_MODBUS_ADDRESS
  const startAddressText = storageMemory
    ? formatStorageStartAddressText(startAddress, storageAddressWidth)
    : formatStartAddressText(startAddress, addressMax)
  const endAddressText = storageMemory
    ? (addressOverflow ? `0x${padStorageHex(addressMax, storageAddressWidth)}+` : `0x${padStorageHex(endAddress, storageAddressWidth)}`)
    : (addressOverflow ? `0x${padWordHex(addressMax)}+` : `0x${padWordHex(endAddress)}`)
  const scalarRegister = storageScalarGroup ? (registers[0] || null) : null
  const scalarDefinitionText = scalarRegister ? createScalarDefinitionText(scalarRegister, baseGroup) : ''
  const scalarEnumDefinitionText = scalarRegister ? createScalarEnumDefinitionText(scalarRegister, baseGroup) : ''
  const scalarInputValueText = scalarRegister ? formatScalarInputValue(scalarRegister) : ''
  const scalarRawHexText = scalarRegister ? scalarRegister.rawValueText : ''
  const scalarValueText = scalarRegister ? formatScalarDecimalValue(scalarRegister) : ''
  const isStorageEnumScalar = !!(storageScalarGroup && scalarRegister && isEnumLikeRegister(baseGroup, scalarRegister))
  const displayName = storageMemory
    ? stripStorageAreaPrefix(baseGroup.name)
    : baseGroup.name
  const storageCardMetaParts = [startAddressText]
  if (storageScalarGroup && scalarEnumDefinitionText) {
    storageCardMetaParts.push(scalarEnumDefinitionText)
  }
  const storageCardMetaText = storageMemory
    ? storageCardMetaParts.filter(Boolean).join(' ')
    : ''
  const listMetaText = storageMemory
    ? (sourceMetadataLocked
      ? ''
      : (storageScalarGroup
      ? [
        getStorageAreaText(baseGroup),
        startAddressText,
        `${byteLength}B`,
        scalarRawHexText,
        scalarDefinitionText
      ].filter(Boolean).join(' ')
      : `${getStorageAreaText(baseGroup)} ${startAddressText}-${endAddressText} ${byteLength}B`))
    : `${formatAddressRange(startAddress, addressSpan)} · ${quantity}/${wordQuantity}${createGroupSourceMetaText(baseGroup) ? ` · ${createGroupSourceMetaText(baseGroup)}` : ''}${addressOverflow ? ' · 地址超出 0xFFFF' : ''}`
  const detailMetaText = storageMemory
    ? (sourceMetadataLocked ? '' : `${getStorageAreaText(baseGroup, true)} ${startAddressText} ${quantity}/${byteLength}B`)
    : `${formatAddressRange(startAddress, addressSpan)} · ${quantity}/${wordQuantity}${createGroupSourceMetaText(baseGroup) ? ` · ${createGroupSourceMetaText(baseGroup)}` : ''}${addressOverflow ? ' · 地址超出 0xFFFF' : ''}`
  const sourceMetaText = createGroupSourceMetaText(baseGroup)

  return {
    ...baseGroup,
    addressRangeText: storageMemory
      ? formatStorageAddressRange(startAddress, addressSpan, storageAddressWidth)
      : formatAddressRange(startAddress, addressSpan),
    addressOverflow,
    addressWarningText: addressOverflow
      ? `地址超出 0x${storageMemory ? padStorageHex(addressMax, storageAddressWidth) : padWordHex(addressMax)}`
      : '',
    detailMetaText,
    detailTitleText: displayName,
    displayName,
    endAddressText,
    functionCode: registerType.functionCode,
    isReadOnly: !registerType.writable,
    isStorageEnumScalar,
    isStorageScalarGroup: storageScalarGroup,
    byteLength,
    listMetaText,
    maxQuantity,
    registerTypeIndex: getRegisterTypeIndex(registerType.key),
    registerTypeText: registerType.label,
    registers,
    paddedByteLength,
    scalarDefinitionText,
    scalarEnumDefinitionText,
    scalarInputValueText,
    scalarRawHexText,
    scalarValueText,
    sourceMetaText,
    sourceMetadataLocked,
    startAddressText,
    storageCardMetaText,
    wordQuantity,
    writable: registerType.writable
  }
}

function normalizeGroupConfig(config = {}) {
  const registerType = config.registerTypeIndex !== undefined && config.registerTypeIndex !== null
    ? (REGISTER_TYPE_OPTIONS[Number(config.registerTypeIndex)] || REGISTER_TYPE_OPTIONS[0])
    : getRegisterType(config.registerType || config.type || DEFAULT_REGISTER_TYPE)
  const maxQuantity = getMaxQuantity(registerType.key)
  const isStorageConfig = !!String(config.sourceMemoryArea || '').trim()
    || config.addressUnit === 'byte'
    || config.addressUnit === 'bytes'
  const storageAddressWidth = isStorageConfig ? resolveStorageAddressWidth(config, 0) : 0
  const storageAddressByteLength = storageAddressWidth === 16 ? 2 : 4

  return {
    ...(isStorageConfig ? {
      addressUnit: config.addressUnit || 'byte',
      sourceAddressByteLength: config.sourceAddressByteLength || storageAddressByteLength,
      sourceAddressWidth: storageAddressWidth,
      sourceMemoryArea: config.sourceMemoryArea || 'XDATA'
    } : {}),
    layout: isStructLayout(config.layout) ? GROUP_LAYOUT_STRUCT : GROUP_LAYOUT_REGISTER,
    name: String(config.name || config.groupName || '寄存器组').trim() || '寄存器组',
    pollEnabled: config.pollEnabled === false ? false : true,
    quantity: parseConfigQuantity(config.quantity, maxQuantity),
    registerType: registerType.key,
    startAddress: parseConfigAddress(config.startAddress, {
      label: isStorageConfig ? '内存起始地址' : '寄存器起始地址',
      maxAddress: isStorageConfig ? getStorageAddressMax(storageAddressWidth) : MAX_MODBUS_ADDRESS
    })
  }
}

function getRegisterJsonValue(register) {
  if (register.inputValue !== undefined && register.inputValue !== null) {
    return normalizeTextValue(register.inputValue)
  }

  if (register.defaultValue !== undefined && register.defaultValue !== null) {
    return normalizeTextValue(register.defaultValue)
  }

  if (register.displayValue !== undefined && register.displayValue !== null && register.displayValue !== '--') {
    return normalizeTextValue(register.displayValue)
  }

  return ''
}

function normalizeImportedRegisterDataType(register) {
  const dataType = register.dataType || register.type || DEFAULT_DATA_TYPE

  return getDataType(dataType).key
}

function cloneImportedGroup(group) {
  return {
    layout: isStructLayout(group.layout) ? GROUP_LAYOUT_STRUCT : GROUP_LAYOUT_REGISTER,
    name: group.name,
    pollEnabled: group.pollEnabled === false ? false : true,
    quantity: group.quantity,
    registerType: group.registerType || group.type || DEFAULT_REGISTER_TYPE,
    registers: (Array.isArray(group.registers) ? group.registers : []).map((register) => ({
      conversionFormula: register.conversionFormula,
      dataType: normalizeImportedRegisterDataType(register),
      defaultValue: register.defaultValue,
      inputValue: register.inputValue,
      maxValue: register.maxValue,
      minValue: register.minValue,
      name: register.name,
      isStructField: !!register.isStructField,
      textByteLength: register.textByteLength,
      remark: register.remark,
      unit: register.unit,
      value: register.value,
      ...pickFields(register, STRUCT_REGISTER_FIELDS),
      ...pickFields(register, SOURCE_REGISTER_FIELDS)
    })),
    startAddress: group.startAddress,
    ...pickFields(group, SOURCE_GROUP_FIELDS)
  }
}

module.exports = {
  DATA_TYPE_OPTIONS,
  DEFAULT_DATA_TYPE,
  DEFAULT_REGISTER_TYPE,
  GROUP_LAYOUT_REGISTER,
  GROUP_LAYOUT_STRUCT,
  MAX_MODBUS_ADDRESS,
  MAX_STORAGE_ADDRESS,
  REGISTER_TYPE_OPTIONS,
  cloneImportedGroup,
  decodeRegisterFromByteCache,
  decodeRegisterFromWordCache,
  decodeRegisterValue,
  formatCoilDisplayValue,
  formatRegisterValue,
  getDataType,
  getRegisterEncodedBytes,
  getRegisterEncodedWords,
  getGroupEncodedBytes,
  getGroupEncodedWords,
  getRegisterWordCount,
  getRegisterJsonValue,
  getRegisterBytesFromByteCache,
  getRegisterWordsFromByteCache,
  getRegisterWordsFromWordCache,
  getRegisterWriteValueText,
  isAddressRangeOverflow,
  isBitRegisterType,
  isByteRegister,
  isParameterGroupPollEnabled,
  isStorageMemoryGroup,
  isStorageScalarGroup,
  isStorageSourceLockedGroup,
  isStorageSourceLockedRegister,
  isStorageStructGroup,
  isTextRegister,
  normalizeGroup,
  normalizeGroupConfig,
  normalizeStorageCodeInfoCard,
  normalizeRegister,
  parseCoilValue,
  registerTypeIsBit,
  splitWordSpans,
  validateRegisterValue
}
