const {
  padHex
} = require('../../utils/base-utils.js')
const {
  bytesToWords,
  toByteArray
} = require('../../utils/binary-utils.js')
const {
  BYTE_ORDER_HIGH,
  appendCrc16Ccitt,
  crc16Ccitt,
  hasValidCrc16Ccitt
} = require('../../utils/crc.js')
const transport = require('../../transport/ble-core.js')

const PROTOCOL_NAME = 'storage-access'

const CMD_ERR_MASK = 0x80
const CMD_WRITE_MASK = 0x40
const CMD_AREA_MASK = 0x3F
const CMD_INFO = 0x0F
const INFO_DATA_BYTE_LENGTH = 4

const AREA = {
  DATA: 0x01,
  IDATA: 0x02,
  XDATA: 0x03,
  CODE: 0x04,
  INFO: 0x0F
}

const AREA_NAMES = {
  [AREA.DATA]: 'DATA',
  [AREA.IDATA]: 'IDATA',
  [AREA.XDATA]: 'XDATA',
  [AREA.CODE]: 'CODE',
  [AREA.INFO]: 'INFO'
}

const AREA_BY_NAME = {
  DATA: AREA.DATA,
  IDATA: AREA.IDATA,
  XDATA: AREA.XDATA,
  CODE: AREA.CODE,
  INFO: AREA.INFO,
  SYNC: AREA.INFO
}

const EXCEPTION_MESSAGES = {
  0x01: '非法命令',
  0x02: '非法区域',
  0x03: '非法地址',
  0x04: '非法长度',
  0x05: '写保护',
  0x06: '设备忙',
  0x07: '格式错误',
  0x08: '访问被拒绝',
  0x09: '内部错误',
  0x0A: '对齐错误',
  0x0B: '范围溢出',
  0x0C: '不支持的操作'
}

const DEFAULT_MAX_FRAME_BYTES = 64
const MAX_PAYLOAD_BYTES = 256
const UNLIMITED_FRAME_BYTES = 0

const READ_REQUEST_LENGTH = 7
const WRITE_REQUEST_OVERHEAD = 7
const READ_RESPONSE_OVERHEAD = 7
const WRITE_RESPONSE_LENGTH = 7
const EXCEPTION_RESPONSE_LENGTH = 4
const INFO_REQUEST_LENGTH = READ_REQUEST_LENGTH
const INFO_RESPONSE_LENGTH = READ_RESPONSE_OVERHEAD + INFO_DATA_BYTE_LENGTH

const STORAGE_CRC_OPTIONS = {
  byteOrder: BYTE_ORDER_HIGH
}
const VALID_AREAS = [AREA.DATA, AREA.IDATA, AREA.XDATA, AREA.CODE, AREA.INFO]
const MEMORY_AREAS = [AREA.DATA, AREA.IDATA, AREA.XDATA, AREA.CODE]

function toByte(value, label) {
  if (!Number.isInteger(value) || value < 0 || value > 0xFF) {
    throw new Error(`${label}必须在 0x00 至 0xFF 之间`)
  }

  return value
}

function toWord(value, label) {
  if (!Number.isInteger(value) || value < 0 || value > 0xFFFF) {
    throw new Error(`${label}必须在 0x0000 至 0xFFFF 之间`)
  }

  return value
}

function normalizeArea(value) {
  if (typeof value === 'string') {
    const area = AREA_BY_NAME[value.trim().toUpperCase()]
    if (area) return area
  }

  const area = toByte(Number(value), '存储区域')
  if (VALID_AREAS.indexOf(area) < 0) {
    throw new Error('存储区域必须为 data/idata/xdata/code/info')
  }

  return area
}

function normalizeMemoryArea(value) {
  const area = normalizeArea(value)
  if (MEMORY_AREAS.indexOf(area) < 0) {
    throw new Error('存储读写区域必须为 data/idata/xdata/code')
  }

  return area
}

function toByteLength(value, label = '字节长度', maxPayload = MAX_PAYLOAD_BYTES) {
  const byteLength = toWord(Number(value), label)
  if (byteLength === 0) {
    throw new Error(`${label}必须大于 0`)
  }
  if (maxPayload > 0 && byteLength > maxPayload) {
    throw new Error(`单帧最多访问 ${maxPayload} 字节`)
  }

  return byteLength
}

function splitWord(value) {
  return [(value >> 8) & 0xFF, value & 0xFF]
}

function readWord(bytes, offset) {
  return (((bytes[offset] || 0) << 8) | (bytes[offset + 1] || 0)) & 0xFFFF
}

function normalizeMaxFrameBytes(maxFrameBytes = DEFAULT_MAX_FRAME_BYTES) {
  const numberValue = Number(maxFrameBytes)
  if (Number.isFinite(numberValue) && Math.round(numberValue) === UNLIMITED_FRAME_BYTES) return UNLIMITED_FRAME_BYTES
  if (Number.isFinite(numberValue) && numberValue > 0) return Math.round(numberValue)

  return DEFAULT_MAX_FRAME_BYTES
}

function getPayloadLimitFromFrame(maxFrameBytes, overhead) {
  const frameBytes = normalizeMaxFrameBytes(maxFrameBytes)
  if (frameBytes === UNLIMITED_FRAME_BYTES) return MAX_PAYLOAD_BYTES

  return Math.max(0, Math.min(MAX_PAYLOAD_BYTES, frameBytes - overhead))
}

function getMaxReadByteLength(maxFrameBytes = DEFAULT_MAX_FRAME_BYTES) {
  return getPayloadLimitFromFrame(maxFrameBytes, READ_RESPONSE_OVERHEAD)
}

function getMaxWriteByteLength(maxFrameBytes = DEFAULT_MAX_FRAME_BYTES) {
  return getPayloadLimitFromFrame(maxFrameBytes, WRITE_REQUEST_OVERHEAD)
}

function buildCommand(area, isWrite = false) {
  const normalizedArea = isWrite ? normalizeMemoryArea(area) : normalizeArea(area)

  return (isWrite ? CMD_WRITE_MASK : 0x00) | normalizedArea
}

function decodeCommand(command) {
  const cmd = toByte(Number(command), '命令字')

  return {
    area: cmd & CMD_AREA_MASK,
    command: cmd,
    hasError: !!(cmd & CMD_ERR_MASK),
    isWrite: !!(cmd & CMD_WRITE_MASK)
  }
}

function hasValidStorageCrc(bytes) {
  const frame = toByteArray(bytes)
  if (frame.length < 3) return false
  if (frame.length >= 4) return hasValidCrc16Ccitt(frame, STORAGE_CRC_OPTIONS)

  const expected = crc16Ccitt(frame.slice(0, -2), STORAGE_CRC_OPTIONS)
  const received = (((frame[frame.length - 2] || 0) << 8) | (frame[frame.length - 1] || 0)) & 0xFFFF
  return expected === received
}

function appendStorageCrc(bytes) {
  return appendCrc16Ccitt(bytes, STORAGE_CRC_OPTIONS)
}

function buildReadFrame(area, address, byteLength, options = {}) {
  const command = buildCommand(area, false)
  const startAddress = toWord(Number(address), '内存地址')
  const maxByteLength = getMaxReadByteLength(options.maxFrameBytes)
  const length = toByteLength(Number(byteLength), '读取字节长度', maxByteLength || MAX_PAYLOAD_BYTES)

  return appendStorageCrc([command].concat(splitWord(startAddress), splitWord(length)))
}

function buildInfoFrame(address = 0, byteLength = INFO_DATA_BYTE_LENGTH) {
  return buildReadFrame(AREA.INFO, address, byteLength)
}

function buildWriteFrame(area, address, bytes, options = {}) {
  const normalizedArea = normalizeMemoryArea(area)
  if (normalizedArea === AREA.CODE) {
    throw new Error('code 区暂不支持写入')
  }

  const command = buildCommand(normalizedArea, true)
  const startAddress = toWord(Number(address), '内存地址')
  const dataBytes = toByteArray(bytes).map((byte) => toByte(Number(byte), '写入字节'))
  const maxByteLength = getMaxWriteByteLength(options.maxFrameBytes)
  const length = toByteLength(dataBytes.length, '写入字节长度', maxByteLength || MAX_PAYLOAD_BYTES)

  return appendStorageCrc([command].concat(splitWord(startAddress), splitWord(length), dataBytes))
}

function formatHex(bytes) {
  return toByteArray(bytes).map((byte) => byte.toString(16).padStart(2, '0').toUpperCase()).join(' ')
}

function parseStorageAccessResponse(bytes) {
  const frame = toByteArray(bytes)
  if (frame.length < EXCEPTION_RESPONSE_LENGTH || !hasValidStorageCrc(frame)) return null

  const command = frame[0] & 0xFF
  const decoded = decodeCommand(command)

  if (decoded.hasError) {
    if (frame.length !== EXCEPTION_RESPONSE_LENGTH) return null

    return {
      area: decoded.area,
      areaName: AREA_NAMES[decoded.area] || 'UNKNOWN',
      command,
      exceptionCode: frame[1] & 0xFF,
      isException: true,
      isWrite: decoded.isWrite,
      protocol: PROTOCOL_NAME,
      sourceCommand: command & ~CMD_ERR_MASK
    }
  }

  if (!AREA_NAMES[decoded.area]) return null

  if (decoded.isWrite) {
    if (decoded.area === AREA.INFO) return null
    if (frame.length !== WRITE_RESPONSE_LENGTH) return null

    return {
      address: readWord(frame, 1),
      area: decoded.area,
      areaName: AREA_NAMES[decoded.area] || 'UNKNOWN',
      byteLength: readWord(frame, 3),
      command,
      dataBytes: [],
      isException: false,
      isWrite: true,
      protocol: PROTOCOL_NAME
    }
  }

  if (frame.length < READ_RESPONSE_OVERHEAD) return null

  const byteLength = readWord(frame, 3)
  const dataStart = 5
  const dataEnd = dataStart + byteLength
  if (frame.length !== dataEnd + 2) return null

  const dataBytes = frame.slice(dataStart, dataEnd)

  return {
    address: readWord(frame, 1),
    area: decoded.area,
    areaName: AREA_NAMES[decoded.area] || 'UNKNOWN',
    byteLength,
    command,
    dataBytes,
    isException: false,
    isInfo: decoded.area === AREA.INFO,
    isWrite: false,
    protocol: PROTOCOL_NAME,
    words: bytesToWords(dataBytes.length % 2 === 0 ? dataBytes : dataBytes.concat(0)),
    ...(decoded.area === AREA.INFO && dataBytes.length >= INFO_DATA_BYTE_LENGTH
      ? {
        codeInfoAddress: readWord(frame, 5),
        codeInfoByteLength: readWord(frame, 7),
        infoBytes: dataBytes.slice(0, INFO_DATA_BYTE_LENGTH)
      }
      : {})
  }
}

function parseStorageAccessRequest(bytes) {
  const frame = toByteArray(bytes)
  if (frame.length < INFO_REQUEST_LENGTH || !hasValidStorageCrc(frame)) return null

  const command = frame[0] & 0xFF
  if (frame.length < READ_REQUEST_LENGTH) return null

  const decoded = decodeCommand(command)
  if (decoded.hasError) return null
  if (!AREA_NAMES[decoded.area]) return null
  if (decoded.area === AREA.INFO && decoded.isWrite) return null

  const address = readWord(frame, 1)
  const byteLength = readWord(frame, 3)
  const expectedLength = decoded.isWrite
    ? WRITE_REQUEST_OVERHEAD + byteLength
    : READ_REQUEST_LENGTH

  if (byteLength <= 0 || frame.length !== expectedLength) return null

  return {
    address,
    area: decoded.area,
    areaName: AREA_NAMES[decoded.area] || 'UNKNOWN',
    byteLength,
    command,
    dataBytes: decoded.isWrite ? frame.slice(5, 5 + byteLength) : [],
    isInfo: decoded.area === AREA.INFO,
    isWrite: decoded.isWrite,
    kind: 'raw-hex',
    operation: decoded.isWrite ? 'write' : 'read',
    protocol: PROTOCOL_NAME,
    quantity: byteLength
  }
}

function getExpectedResponseLength(expected, responseCommand, responseBytes = []) {
  if (!expected) return 0
  const command = Number(responseCommand) & 0xFF
  if (command === (expected.command | CMD_ERR_MASK)) return EXCEPTION_RESPONSE_LENGTH
  if (command !== expected.command) return 0

  if (expected.operation === 'write' || expected.isWrite) {
    return WRITE_RESPONSE_LENGTH
  }

  if (responseBytes.length < 5) return 0

  return READ_RESPONSE_OVERHEAD + readWord(responseBytes, 3)
}

function isExpectedResponse(response, expected) {
  if (!response || !expected) return false
  const sourceCommand = response.isException ? response.sourceCommand : response.command
  if (sourceCommand !== expected.command) return false
  if (!response.isException && response.area !== expected.area) return false
  if (response.isException) return true
  if (response.address !== expected.address) return false
  if (response.byteLength !== expected.byteLength) return false
  if (!response.isWrite && (!Array.isArray(response.dataBytes) || response.dataBytes.length !== expected.byteLength)) return false

  return true
}

function getExceptionText(code) {
  return EXCEPTION_MESSAGES[code] || '未知异常'
}

function formatExceptionMessage(response) {
  const sourceCommand = response && response.sourceCommand
  const exceptionCode = response && response.exceptionCode
  const exceptionText = getExceptionText(exceptionCode)

  return `设备返回异常帧：命令 0x${padHex(sourceCommand, 2)}，异常码 0x${padHex(exceptionCode, 2)}（${exceptionText}）`
}

function getReadBufferHint(expected) {
  if (!expected) return 0
  if (expected.operation === 'write' || expected.isWrite) return WRITE_RESPONSE_LENGTH

  return READ_RESPONSE_OVERHEAD + Number(expected.byteLength || expected.quantity || 0)
}

function alignResponseBuffer(buffer, expected) {
  if (!Array.isArray(buffer) || !buffer.length || !expected) return

  const expectedCommands = [expected.command, expected.command | CMD_ERR_MASK]
  let matchIndex = -1

  for (let index = 0; index < buffer.length; index += 1) {
    if (expectedCommands.indexOf(buffer[index]) < 0) continue

    matchIndex = index
    break
  }

  if (matchIndex > 0) {
    buffer.splice(0, matchIndex)
  } else if (matchIndex < 0 && buffer.length > 1) {
    buffer.splice(0, buffer.length - 1)
  }
}

function readResponseFromBuffer(buffer, expected, options = {}) {
  if (!Array.isArray(buffer) || !buffer.length || !expected) {
    return {
      status: 'pending'
    }
  }

  alignResponseBuffer(buffer, expected)

  while (buffer.length >= 1) {
    const responseCommand = buffer[0]
    const responseLength = getExpectedResponseLength(expected, responseCommand, buffer)

    if (!responseLength) {
      return {
        status: 'pending'
      }
    }

    const frameLimit = normalizeMaxFrameBytes(
      options.maxFrameBytes === undefined ? expected.maxFrameBytes : options.maxFrameBytes
    )
    if (frameLimit > 0 && responseLength > frameLimit) {
      return {
        frameLimit,
        responseLength,
        status: 'frame-too-long'
      }
    }

    if (buffer.length < responseLength) {
      return {
        status: 'pending'
      }
    }

    const frameBytes = buffer.slice(0, responseLength)
    const response = parseStorageAccessResponse(frameBytes)
    if (!response) {
      return {
        frameBytes,
        status: 'invalid'
      }
    }

    if (!isExpectedResponse(response, expected)) {
      buffer.shift()
      alignResponseBuffer(buffer, expected)
      continue
    }

    if (response.isException) {
      return {
        message: formatExceptionMessage(response),
        response,
        status: 'exception'
      }
    }

    buffer.splice(0, responseLength)
    return {
      response,
      status: 'complete'
    }
  }

  return {
    status: 'pending'
  }
}

function createExpected(area, address, byteLength, isWrite, kind) {
  const normalizedArea = normalizeMemoryArea(area)
  const command = buildCommand(normalizedArea, isWrite)

  return {
    address,
    area: normalizedArea,
    byteLength,
    command,
    isWrite,
    kind,
    operation: isWrite ? 'write' : 'read',
    protocol: PROTOCOL_NAME,
    quantity: byteLength
  }
}

function createInfoExpected(kind = 'storage-info-read') {
  return {
    address: 0,
    area: AREA.INFO,
    byteLength: INFO_DATA_BYTE_LENGTH,
    command: buildCommand(AREA.INFO, false),
    frame: buildInfoFrame(0, INFO_DATA_BYTE_LENGTH),
    isInfo: true,
    isWrite: false,
    kind,
    operation: 'read',
    protocol: PROTOCOL_NAME
  }
}

function formatAddress(value) {
  return Number(value || 0).toString(16).toUpperCase()
}

function getChunkLabel(label, chunks, chunk) {
  if (!label || chunks.length <= 1) return label

  return `${label} ${formatAddress(chunk.address)}-${formatAddress(chunk.address + chunk.quantity - 1)}`
}

function splitQuantity(startAddress, quantity, maxQuantity) {
  const chunks = []
  let address = Number(startAddress) || 0
  let remaining = Math.max(0, Math.floor(Number(quantity) || 0))
  const chunkLimit = Math.max(1, Math.floor(Number(maxQuantity) || remaining || 1))

  while (remaining > 0) {
    const chunkQuantity = Math.min(remaining, chunkLimit)
    chunks.push({
      address,
      quantity: chunkQuantity
    })
    address += chunkQuantity
    remaining -= chunkQuantity
  }

  return chunks
}

function getReadChunks(startAddress, byteLength, options = {}) {
  const maxByteLength = getMaxReadByteLength(options.maxFrameBytes)

  return splitQuantity(startAddress, byteLength, maxByteLength || byteLength)
}

function getWriteChunks(startAddress, bytes, options = {}) {
  const sourceBytes = Array.prototype.slice.call(bytes || []).map((byte) => Number(byte) & 0xFF)
  const maxByteLength = getMaxWriteByteLength(options.maxFrameBytes)
  const chunks = splitQuantity(startAddress, sourceBytes.length, maxByteLength || sourceBytes.length)
  let offset = 0

  return chunks.map((chunk) => {
    const dataBytes = sourceBytes.slice(offset, offset + chunk.quantity)
    offset += chunk.quantity

    return {
      ...chunk,
      dataBytes
    }
  })
}

function sendReadChunk(area, chunk, label, kind, options = {}) {
  const normalizedArea = normalizeMemoryArea(area)

  return transport.sendManagedFrame(
    buildReadFrame(normalizedArea, chunk.address, chunk.quantity, {
      maxFrameBytes: options.maxFrameBytes
    }),
    label,
    createExpected(normalizedArea, chunk.address, chunk.quantity, false, kind),
    {
      maxFrameBytes: options.maxFrameBytes,
      showModal: options.showModal
    }
  )
}

async function readMemory(area, startAddress, byteLength, label, kind = 'storage-memory-read', options = {}) {
  const normalizedArea = normalizeMemoryArea(area)
  const bytes = []
  const chunks = getReadChunks(startAddress, byteLength, options)

  for (const chunk of chunks) {
    const response = await sendReadChunk(
      normalizedArea,
      chunk,
      getChunkLabel(label, chunks, chunk),
      kind,
      options
    )
    if (!response) return null

    const dataBytes = Array.isArray(response.dataBytes) ? response.dataBytes : []
    dataBytes.forEach((byte, index) => {
      bytes[chunk.address - startAddress + index] = Number(byte) & 0xFF
    })

    if (typeof options.onChunk === 'function') {
      options.onChunk(response, chunk)
    }
  }

  return bytes
}

async function writeMemory(area, startAddress, bytes, label, kind = 'storage-memory-write', options = {}) {
  const normalizedArea = normalizeMemoryArea(area)
  const chunks = getWriteChunks(startAddress, bytes, options)

  for (const chunk of chunks) {
    const response = await transport.sendManagedFrame(
      buildWriteFrame(normalizedArea, chunk.address, chunk.dataBytes, {
        maxFrameBytes: options.maxFrameBytes
      }),
      getChunkLabel(label, chunks, chunk),
      createExpected(normalizedArea, chunk.address, chunk.quantity, true, kind),
      {
        maxFrameBytes: options.maxFrameBytes,
        showModal: options.showModal
      }
    )
    if (!response) return false

    if (typeof options.onChunk === 'function') {
      options.onChunk(response, chunk)
    }
  }

  return true
}

async function readCodeInfoBlock(label = '同步info', kind = 'storage-info-read', options = {}) {
  const infoResponse = await transport.sendManagedFrame(
    buildInfoFrame(0, INFO_DATA_BYTE_LENGTH),
    label,
    createInfoExpected(`${kind}-info`),
    {
      maxFrameBytes: options.maxFrameBytes,
      showModal: options.showModal
    }
  )
  if (!infoResponse) return null

  const codeInfoAddress = Number(infoResponse.codeInfoAddress || 0)
  const codeInfoByteLength = Number(infoResponse.codeInfoByteLength || 0)
  if (!codeInfoByteLength || codeInfoByteLength > 0xFFFF) {
    transport.showCommandAlert(label, 'info 信息块长度无效')
    return null
  }

  const bytes = await readMemory(
    AREA.CODE,
    codeInfoAddress,
    codeInfoByteLength,
    label,
    kind,
    options
  )
  if (!bytes) return null

  return {
    codeInfoAddress,
    codeInfoByteLength,
    codeInfoBytes: bytes,
    infoBytes: Array.isArray(infoResponse.infoBytes)
      ? infoResponse.infoBytes
      : [],
    codeInfoMemoryType: AREA.CODE
  }
}

const response = {
  createExpected,
  createInfoExpected,
  formatExceptionMessage,
  getExceptionText,
  getExpectedResponseLength,
  getReadBufferHint,
  isExpectedResponse,
  parseStorageAccessRequest,
  parseStorageAccessResponse,
  readResponseFromBuffer
}

const client = {
  AREA,
  getMaxReadByteLength,
  getMaxWriteByteLength,
  getReadChunks,
  getWriteChunks,
  readCodeInfoBlock,
  readMemory,
  splitQuantity,
  writeMemory
}

module.exports = {
  AREA,
  AREA_BY_NAME,
  AREA_NAMES,
  CMD_AREA_MASK,
  CMD_ERR_MASK,
  CMD_INFO,
  CMD_WRITE_MASK,
  DEFAULT_MAX_FRAME_BYTES,
  EXCEPTION_MESSAGES,
  EXCEPTION_RESPONSE_LENGTH,
  INFO_REQUEST_LENGTH,
  INFO_RESPONSE_LENGTH,
  INFO_DATA_BYTE_LENGTH,
  MAX_PAYLOAD_BYTES,
  PROTOCOL_NAME,
  READ_REQUEST_LENGTH,
  READ_RESPONSE_OVERHEAD,
  STORAGE_CRC_OPTIONS,
  UNLIMITED_FRAME_BYTES,
  WRITE_REQUEST_OVERHEAD,
  WRITE_RESPONSE_LENGTH,
  appendStorageCrc,
  buildCommand,
  buildInfoFrame,
  buildReadFrame,
  buildWriteFrame,
  client,
  decodeCommand,
  formatHex,
  getMaxReadByteLength,
  getMaxWriteByteLength,
  hasValidStorageCrc,
  normalizeArea,
  normalizeMaxFrameBytes,
  normalizeMemoryArea,
  response,
  splitWord,
  toByte,
  toByteLength,
  toWord
}
