const {
  parseHexInteger
} = require('../../utils/base-utils.js')
const {
  bytesToWords
} = require('../../utils/binary-utils.js')
const transport = require('../../transport/ble-core.js')
const modbusClient = require('../modbus-rtu/service.js')
const storageAccessService = require('../storage-access/service.js')
const {
  decodeRegisterFromByteCache,
  decodeRegisterFromWordCache,
  decodeRegisterValue,
  formatCoilDisplayValue,
  formatRegisterValue,
  getDataType,
  getGroupEncodedBytes,
  getGroupEncodedWords,
  getRegisterBytesFromByteCache,
  getRegisterEncodedBytes,
  getRegisterEncodedWords,
  getRegisterWordsFromByteCache,
  getRegisterWordsFromWordCache,
  getRegisterWriteValueText,
  isBitRegisterType,
  isByteRegister,
  normalizeRegister,
  parseCoilValue,
  registerTypeIsBit,
  splitWordSpans
} = require('../../domain/parameter-groups/model.js')

const STORAGE_ACCESS_READ_ONLY_AREAS = [storageAccessService.AREA.CODEINFO, storageAccessService.AREA.CODE]
const MAX_STORAGE_ACCESS_ADDRESS = 0xFFFFFFFF
const MAX_STORAGE_ACCESS_BYTE_LENGTH = 0xFFFFFFFF

function getMemoryType(group = {}) {
  const memoryArea = String(group.sourceMemoryArea || '').trim().toUpperCase()
  if (memoryArea === 'ADDR32' || memoryArea === 'ADDRESS32') return storageAccessService.AREA.ADDR32
  if (memoryArea === 'BIT') return storageAccessService.AREA.DATA

  const area = storageAccessService.AREA[memoryArea]

  return area === undefined ? null : area
}

function isReadOnlyMemoryType(memoryType) {
  return STORAGE_ACCESS_READ_ONLY_AREAS.indexOf(memoryType) >= 0
}

function isMemoryGroup(group = {}) {
  return getMemoryType(group) !== null
}

function isByteAddressedGroup(group = {}) {
  return group.addressUnit === 'byte' || group.addressUnit === 'bytes' || isMemoryGroup(group)
}

function getMemoryAddress(group = {}) {
  const sourceAddress = Number(group.sourceAddress)
  if (Number.isFinite(sourceAddress)) return Math.min(MAX_STORAGE_ACCESS_ADDRESS, Math.max(0, Math.floor(sourceAddress)))

  return Math.min(MAX_STORAGE_ACCESS_ADDRESS, Math.max(0, Math.floor(Number(group.startAddress) || 0)))
}

function getMemoryByteLength(group = {}) {
  const sourceByteLength = Number(group.sourceByteLength)
  if (Number.isFinite(sourceByteLength) && sourceByteLength > 0) return Math.min(MAX_STORAGE_ACCESS_BYTE_LENGTH, Math.ceil(sourceByteLength))

  const byteLength = Number(group.byteLength)
  if (Number.isFinite(byteLength) && byteLength > 0) return Math.min(MAX_STORAGE_ACCESS_BYTE_LENGTH, Math.ceil(byteLength))

  return Math.max(1, Math.ceil((Number(group.wordQuantity) || 1) * 2))
}

function shouldUseStorageAccess(options = {}, group = {}) {
  return !!options.useStorageAccess && isMemoryGroup(group)
}

function getStorageMaxPacketLength(group = {}, options = {}) {
  const configuredMaxPacketLength = storageAccessService.resolveMaxPacketLength(options.maxPacketLength)
  const deviceMaxPacketLength = Number(group.codeInfoContext && group.codeInfoContext.maxPacketLength)
  if (!Number.isFinite(deviceMaxPacketLength) || deviceMaxPacketLength <= 0) return configuredMaxPacketLength
  if (configuredMaxPacketLength === 0) return Math.round(deviceMaxPacketLength)

  return Math.min(configuredMaxPacketLength, Math.round(deviceMaxPacketLength))
}

function normalizeNumericCache(source = {}) {
  const cache = {}

  Object.keys(source || {}).forEach((addressText) => {
    const numericAddress = Number(addressText)
    if (Number.isFinite(numericAddress)) {
      cache[numericAddress] = Number(source[addressText]) & 0xFFFF
    }
  })

  return cache
}

function createRegisterStateFromCache(group, register, wordCache) {
  const rawBytes = registerTypeIsBit(register)
    ? []
    : (isByteAddressedGroup(group) ? getRegisterBytesFromByteCache(register, wordCache) : [])
  const rawWords = registerTypeIsBit(register)
    ? []
    : (isByteAddressedGroup(group)
      ? getRegisterWordsFromByteCache(register, wordCache)
      : getRegisterWordsFromWordCache(register, wordCache))
  const rawValue = registerTypeIsBit(register)
    ? decodeRegisterFromWordCache(register, wordCache)
    : (isByteAddressedGroup(group)
      ? decodeRegisterFromByteCache(register, wordCache)
      : (rawWords ? decodeRegisterValue(register, rawWords) : null))
  const displayValue = rawValue === null || rawValue === undefined
    ? '--'
    : (registerTypeIsBit(register)
      ? formatCoilDisplayValue(rawValue)
      : formatRegisterValue(register, rawValue))
  const preNormalizedRegister = {
    ...register,
    displayValue,
    isDirty: false,
    rawBytes: rawBytes || [],
    rawValue,
    rawWords: rawWords || []
  }
  const nextRegister = normalizeRegister(preNormalizedRegister, group, 0, register.address, register.byteOffset)

  return {
    ...nextRegister,
    id: register.id,
    inputValue: group.writable ? displayValue : register.inputValue
  }
}

function applyReadCacheToGroup(group, wordCache) {
  return {
    ...group,
    registers: group.registers.map((register) => createRegisterStateFromCache(group, register, wordCache))
  }
}

function applyWrittenSnapshotToRegister(register, snapshot = {}) {
  const hasDisplayValue = Object.prototype.hasOwnProperty.call(snapshot, 'displayValue')
  const hasRawBytes = Object.prototype.hasOwnProperty.call(snapshot, 'rawBytes')
  const hasRawValue = Object.prototype.hasOwnProperty.call(snapshot, 'rawValue')
  const hasRawWords = Object.prototype.hasOwnProperty.call(snapshot, 'rawWords')

  return {
    ...register,
    displayValue: hasDisplayValue ? snapshot.displayValue : register.displayValue,
    inputValue: hasDisplayValue ? snapshot.displayValue : register.inputValue,
    isDirty: false,
    rawBytes: hasRawBytes ? snapshot.rawBytes : register.rawBytes,
    rawValue: hasRawValue ? snapshot.rawValue : register.rawValue,
    rawWords: hasRawWords ? snapshot.rawWords : register.rawWords
  }
}

function applyWrittenSnapshotsToGroup(group, snapshots = []) {
  let writtenIndex = 0

  return {
    ...group,
    registers: group.registers.map((register, index) => {
      const snapshot = snapshots[writtenIndex] || {}
      writtenIndex += 1

      const nextRegister = applyWrittenSnapshotToRegister(register, snapshot)
      const normalized = normalizeRegister(nextRegister, group, index, nextRegister.address, nextRegister.byteOffset)

      return {
        ...normalized,
        id: register.id,
        inputValue: group.writable ? nextRegister.displayValue : nextRegister.inputValue
      }
    })
  }
}

function applyWrittenSnapshotToGroupRegister(group, registerIndex, snapshot) {
  return {
    ...group,
    registers: group.registers.map((register, currentIndex) => (
      currentIndex === registerIndex
        ? (() => {
          const nextRegister = applyWrittenSnapshotToRegister(register, snapshot || {})
          const normalized = normalizeRegister(nextRegister, group, currentIndex, nextRegister.address, nextRegister.byteOffset)

          return {
            ...normalized,
            id: register.id,
            inputValue: group.writable ? nextRegister.displayValue : nextRegister.inputValue
          }
        })()
        : register
    ))
  }
}

function getWriteSpanMaxQuantity(totalQuantity, maxPacketLength) {
  if (maxPacketLength === 0) return Math.max(1, totalQuantity)

  return Math.max(1, modbusClient.getMaxWriteMultipleRegisterQuantity(maxPacketLength))
}

async function readModbusGroup(group, options = {}) {
  const totalQuantity = Math.max(1, group.wordQuantity || group.quantity || 0)
  const wordCache = {}
  const slaveAddress = modbusClient.getSharedSlaveAddress()
  if (slaveAddress === null) return null

  const values = await modbusClient.readSpans(
    slaveAddress,
    group.functionCode,
    [{
      address: group.startAddress,
      quantity: totalQuantity
    }],
    group.name || '参数组读取',
    'parameter-group-read',
    {
      maxFrameBytes: options.maxPacketLength,
      showModal: options.showModal !== false
    }
  )
  if (!values) return null

  if (isBitRegisterType(group.registerType)) {
    Object.keys(values.coils || {}).forEach((addressText) => {
      wordCache[parseHexInteger(addressText)] = Number(values.coils[addressText]) ? 1 : 0
    })
  } else {
    Object.keys(values.words || {}).forEach((addressText) => {
      wordCache[parseHexInteger(addressText)] = Number(values.words[addressText]) & 0xFFFF
    })
  }

  return wordCache
}

function getRegisterWordsForModbusWrite(group) {
  const words = group.isStructLayout
    ? getGroupEncodedWords(group)
    : Array.from({ length: Math.max(1, group.wordQuantity || 1) }, () => 0)

  if (group.isStructLayout) return words

  for (let index = 0; index < group.registers.length; index += 1) {
    const register = group.registers[index]
    const registerWords = getRegisterEncodedWords(register)

    if (!Array.isArray(registerWords) || !registerWords.length) {
      throw new Error(`${register.name || `寄存器 ${index + 1}`} 没有有效写入值`)
    }

    const dataType = getDataType(register.dataType).key
    const relativeAddress = Math.max(0, register.address - group.startAddress)
    if (isByteRegister(dataType)) {
      const byteValue = Number(registerWords[0]) & 0xFF
      const currentWord = words[relativeAddress] || 0
      words[relativeAddress] = register.byteOffset === 0
        ? (((byteValue << 8) | (currentWord & 0x00FF)) & 0xFFFF)
        : (((currentWord & 0xFF00) | byteValue) & 0xFFFF)
    } else {
      for (let offset = 0; offset < register.registerCount; offset += 1) {
        words[relativeAddress + offset] = Number(registerWords[offset]) & 0xFFFF
      }
    }
  }

  return words
}

function createModbusWrittenRegisterSnapshots(group, words = []) {
  const writtenWordCache = words.reduce((cache, word, offset) => {
    cache[group.startAddress + offset] = word
    return cache
  }, {})

  return group.registers.map((register) => {
    const rawWords = getRegisterWordsFromWordCache(register, writtenWordCache) || []
    const rawValue = decodeRegisterValue(register, rawWords)
    const displayValue = formatRegisterValue(register, rawValue)

    return {
      rawWords,
      rawValue,
      displayValue
    }
  })
}

async function writeModbusCoilGroup(group, options = {}) {
  const slaveAddress = modbusClient.getSharedSlaveAddress()
  if (slaveAddress === null) return null

  const writtenRegisters = []
  for (let index = 0; index < group.registers.length; index += 1) {
    const register = group.registers[index]
    const coilValue = parseCoilValue(getRegisterWriteValueText(register))

    if (coilValue === null) {
      transport.showCommandAlert('参数组写入', `${register.name || `寄存器 ${index + 1}`} 没有有效写入值`)
      return null
    }

    const response = await modbusClient.writeSingleCoil(
      slaveAddress,
      group.startAddress + index,
      !!coilValue,
      register.name || group.name || '参数组写入',
      'parameter-group-coil-write',
      {
        maxFrameBytes: options.maxPacketLength
      }
    )
    if (!response) return null

    writtenRegisters.push({
      rawBytes: [],
      rawValue: coilValue,
      rawWords: [],
      displayValue: formatCoilDisplayValue(coilValue)
    })
  }

  return writtenRegisters
}

async function writeModbusRegisterGroup(group, options = {}) {
  const slaveAddress = modbusClient.getSharedSlaveAddress()
  if (slaveAddress === null) return null

  let words
  try {
    words = getRegisterWordsForModbusWrite(group)
  } catch (error) {
    transport.showCommandAlert('参数组写入', error.message || '寄存器组没有有效写入值')
    return null
  }

  const writtenRegisters = createModbusWrittenRegisterSnapshots(group, words)
  const maxWriteQuantity = getWriteSpanMaxQuantity(words.length, options.maxPacketLength)
  const spans = splitWordSpans(group.startAddress, words.length, maxWriteQuantity)
  let cursor = 0

  for (const span of spans) {
    const spanWords = words.slice(cursor, cursor + span.quantity)
    cursor += span.quantity

    const response = await modbusClient.writeMultipleRegisters(
      slaveAddress,
      span.address,
      spanWords,
      group.name || '参数组写入',
      'parameter-group-write',
      {
        maxFrameBytes: options.maxPacketLength
      }
    )
    if (!response) return null
  }

  return writtenRegisters
}

async function writeModbusGroup(group, options = {}) {
  return group.registerType === 'coil'
    ? writeModbusCoilGroup(group, options)
    : writeModbusRegisterGroup(group, options)
}

function bytesToPaddedWords(bytes = []) {
  return bytesToWords(bytes.length % 2 === 0 ? bytes : bytes.concat(0))
}

function fillByteCacheFromBytes(byteCache, startAddress, bytes = []) {
  bytes.forEach((byte, offset) => {
    byteCache[startAddress + offset] = Number(byte) & 0xFF
  })
}

function fillWordCacheFromBytes(wordCache, startAddress, bytes = []) {
  const words = bytesToPaddedWords(bytes)

  words.forEach((word, offset) => {
    wordCache[startAddress + offset] = Number(word) & 0xFFFF
  })
}

function createStorageWrittenRegisterSnapshots(group, wordCache) {
  return group.registers.map((register) => {
    const rawBytes = isByteAddressedGroup(group)
      ? (getRegisterBytesFromByteCache(register, wordCache) || [])
      : []
    const rawWords = isByteAddressedGroup(group)
      ? (getRegisterWordsFromByteCache(register, wordCache) || [])
      : (getRegisterWordsFromWordCache(register, wordCache) || [])
    const rawValue = isByteAddressedGroup(group)
      ? decodeRegisterFromByteCache(register, wordCache)
      : decodeRegisterValue(register, rawWords)

    return {
      rawBytes,
      rawWords,
      rawValue,
      displayValue: formatRegisterValue(register, rawValue)
    }
  })
}

function createStorageWrittenRegisterSnapshot(group, register, byteCache) {
  const snapshots = createStorageWrittenRegisterSnapshots({
    ...group,
    registers: [register]
  }, byteCache)

  return snapshots[0] || null
}

function groupHasBitFields(group = {}) {
  return (Array.isArray(group.registers) ? group.registers : []).some((register) => !!register.isBitField)
}

async function writeMemoryRegister(group, register, options = {}) {
  const memoryType = getMemoryType(group)
  const maxPacketLength = getStorageMaxPacketLength(group, options)
  const byteLength = Math.max(1, Number(register.byteLength) || 1)
  const address = Math.min(MAX_STORAGE_ACCESS_ADDRESS, Math.max(0, Math.floor(Number(register.address) || getMemoryAddress(group))))
  let bytes

  if (memoryType === null) {
    transport.showCommandAlert('内存写入', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`)
    return null
  }
  if (isReadOnlyMemoryType(memoryType)) {
    transport.showCommandAlert('内存写入', `${group.sourceMemoryArea || '该'} 区不可写`)
    return null
  }

  try {
    if (register.isBitField) {
      const baseBytes = await storageAccessService.readMemory(
        memoryType,
        address,
        byteLength,
        register.name ? `${register.name} 读改写` : '变量读改写',
        'parameter-group-memory-register-rmw-read',
        {
          maxFrameBytes: maxPacketLength
        }
      )
      if (!baseBytes) return null

      bytes = getGroupEncodedBytes({
        ...group,
        paddedByteLength: byteLength,
        registers: [{
          ...register,
          address,
          byteStart: 0
        }]
      }, baseBytes).slice(0, byteLength)
    } else {
      bytes = getRegisterEncodedBytes(register)
    }
  } catch (error) {
    transport.showCommandAlert('内存写入', error.message || '变量没有有效写入值')
    return null
  }

  if (!Array.isArray(bytes) || !bytes.length) {
    transport.showCommandAlert('内存写入', `${register.name || '变量'} 没有有效写入值`)
    return null
  }

  bytes = bytes.slice(0, byteLength)
  while (bytes.length < byteLength) bytes.push(0)

  const ok = await storageAccessService.writeMemory(
    memoryType,
    address,
    bytes,
    register.name || group.name || '变量写入',
    'parameter-group-memory-register-write',
    {
      maxFrameBytes: maxPacketLength
    }
  )
  if (!ok) return null

  const byteCache = {}
  fillByteCacheFromBytes(byteCache, address, bytes)

  return createStorageWrittenRegisterSnapshot(group, register, byteCache)
}

async function readMemoryGroup(group, options = {}) {
  const memoryType = getMemoryType(group)
  const address = getMemoryAddress(group)
  const byteLength = getMemoryByteLength(group)
  const maxPacketLength = getStorageMaxPacketLength(group, options)

  if (memoryType === null) {
    transport.showCommandAlert('内存读取', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`)
    return null
  }

  const bytes = await storageAccessService.readMemory(
    memoryType,
    address,
    byteLength,
    group.name || '内存读取',
    'parameter-group-memory-read',
    {
      maxFrameBytes: maxPacketLength,
      showModal: options.showModal !== false
    }
  )
  if (!bytes) return null

  const wordCache = {}
  if (isByteAddressedGroup(group)) {
    fillByteCacheFromBytes(wordCache, address, bytes)
  } else {
    fillWordCacheFromBytes(wordCache, address, bytes)
  }

  return wordCache
}

async function writeMemoryGroup(group, options = {}) {
  const memoryType = getMemoryType(group)
  const address = getMemoryAddress(group)
  const byteLength = getMemoryByteLength(group)
  const maxPacketLength = getStorageMaxPacketLength(group, options)
  let bytes

  if (memoryType === null) {
    transport.showCommandAlert('内存写入', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`)
    return null
  }
  if (isReadOnlyMemoryType(memoryType)) {
    transport.showCommandAlert('内存写入', `${group.sourceMemoryArea || '该'} 区不可写`)
    return null
  }

  try {
    let baseBytes = null
    if (groupHasBitFields(group)) {
      baseBytes = await storageAccessService.readMemory(
        memoryType,
        address,
        byteLength,
        group.name ? `${group.name} 读改写` : '内存读改写',
        'parameter-group-memory-rmw-read',
        {
          maxFrameBytes: maxPacketLength
        }
      )
      if (!baseBytes) return null
    }

    bytes = getGroupEncodedBytes(group, baseBytes)
  } catch (error) {
    transport.showCommandAlert('内存写入', error.message || '寄存器组没有有效写入值')
    return null
  }

  bytes = bytes.slice(0, byteLength)
  const ok = await storageAccessService.writeMemory(
    memoryType,
    address,
    bytes,
    group.name || '内存写入',
    'parameter-group-memory-write',
    {
      maxFrameBytes: maxPacketLength
    }
  )
  if (!ok) return null

  const wordCache = {}
  if (isByteAddressedGroup(group)) {
    fillByteCacheFromBytes(wordCache, address, bytes)
  } else {
    fillWordCacheFromBytes(wordCache, address, bytes)
  }

  return createStorageWrittenRegisterSnapshots(group, wordCache)
}

async function readGroup(group, options = {}) {
  const wordCache = shouldUseStorageAccess(options, group)
    ? await readMemoryGroup(group, options)
    : await readModbusGroup(group, options)
  if (!wordCache) return null

  const normalizedCache = normalizeNumericCache(wordCache)

  return {
    applyToGroup(currentGroup) {
      return applyReadCacheToGroup(currentGroup, normalizedCache)
    }
  }
}

async function writeRegister(group, registerIndex, options = {}) {
  const register = group && Array.isArray(group.registers) ? group.registers[registerIndex] : null
  if (!register) return null

  if (shouldUseStorageAccess(options, group)) {
    const written = await writeMemoryRegister(group, register, options)

    return written
      ? {
        applyToGroup(currentGroup) {
          return applyWrittenSnapshotToGroupRegister(currentGroup, registerIndex, written)
        }
      }
      : null
  }

  return writeGroup(group, options)
}

async function writeGroup(group, options = {}) {
  const snapshots = shouldUseStorageAccess(options, group)
    ? await writeMemoryGroup(group, options)
    : await writeModbusGroup(group, options)
  if (!snapshots) return null

  return {
    applyToGroup(currentGroup) {
      return applyWrittenSnapshotsToGroup(currentGroup, snapshots)
    }
  }
}

module.exports = {
  getMemoryAddress,
  getMemoryByteLength,
  getMemoryType,
  isByteAddressedGroup,
  isMemoryGroup,
  readGroup,
  writeGroup,
  writeRegister
}
