const {
  padHex
} = require('../../utils/base-utils.js')
const {
  bytesToWords
} = require('../../utils/binary-utils.js')
const {
  BYTE_ORDER_LOW,
  appendCrc16Modbus,
  hasValidCrc16Modbus
} = require('../../utils/crc.js')

const PROTOCOL_NAME = 'modbus-rtu'
const MODBUS_CRC_OPTIONS = {
  byteOrder: BYTE_ORDER_LOW
}
const MAX_MODBUS_DMA_BYTES = 64
const MODBUS_READ_RESPONSE_OVERHEAD = 5
const MODBUS_WRITE_MULTIPLE_REQUEST_OVERHEAD = 9
const MAX_READ_REGISTER_QUANTITY = Math.floor((MAX_MODBUS_DMA_BYTES - MODBUS_READ_RESPONSE_OVERHEAD) / 2)
const MAX_READ_COIL_QUANTITY = (MAX_MODBUS_DMA_BYTES - MODBUS_READ_RESPONSE_OVERHEAD) * 8
const MAX_WRITE_MULTIPLE_REGISTER_QUANTITY = Math.floor((MAX_MODBUS_DMA_BYTES - MODBUS_WRITE_MULTIPLE_REQUEST_OVERHEAD) / 2)
const UNLIMITED_FRAME_BYTES = 0

const MODBUS_EXCEPTION_MESSAGES = {
  0x01: '非法功能',
  0x02: '非法数据地址',
  0x03: '非法数据值',
  0x04: '从站设备故障',
  0x05: '确认',
  0x06: '从站设备忙',
  0x08: '存储奇偶性错误',
  0x0A: '网关路径不可用',
  0x0B: '网关目标设备响应失败'
}

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 splitWord(value) {
  return [(value >> 8) & 0xFF, value & 0xFF]
}

function normalizeMaxFrameBytes(maxFrameBytes = MAX_MODBUS_DMA_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 MAX_MODBUS_DMA_BYTES
}

function getMaxReadQuantity(functionCode, maxFrameBytes = MAX_MODBUS_DMA_BYTES) {
  const frameBytes = normalizeMaxFrameBytes(maxFrameBytes)
  if (frameBytes === UNLIMITED_FRAME_BYTES) return 0xFFFF

  const dataBytes = frameBytes - MODBUS_READ_RESPONSE_OVERHEAD
  if (dataBytes <= 0) return 0
  if (functionCode === 0x01 || functionCode === 0x02) return dataBytes * 8
  if (functionCode === 0x03 || functionCode === 0x04) return Math.floor(dataBytes / 2)

  return 0
}

function getMaxWriteMultipleRegisterQuantity(maxFrameBytes = MAX_MODBUS_DMA_BYTES) {
  const frameBytes = normalizeMaxFrameBytes(maxFrameBytes)
  if (frameBytes === UNLIMITED_FRAME_BYTES) return 0xFFFF

  return Math.max(0, Math.floor((frameBytes - MODBUS_WRITE_MULTIPLE_REQUEST_OVERHEAD) / 2))
}

function buildReadFrame(slaveAddress, functionCode, address, quantity, options = {}) {
  const slave = toByte(slaveAddress, '从站地址')
  const command = toByte(functionCode, '功能码')
  const startAddress = toWord(address, '寄存器地址')
  const registerQuantity = toWord(quantity, '读取数量')
  const maxQuantity = getMaxReadQuantity(command, options.maxFrameBytes)

  if ([0x01, 0x02, 0x03, 0x04].indexOf(command) < 0) {
    throw new Error('当前功能码不是读取命令')
  }
  if (registerQuantity === 0) {
    throw new Error('读取数量必须大于 0')
  }
  if ([0x03, 0x04].indexOf(command) >= 0 && maxQuantity > 0 && registerQuantity > maxQuantity) {
    throw new Error(`单帧最多读取 ${maxQuantity} 个寄存器`)
  }
  if ((command === 0x01 || command === 0x02) && maxQuantity > 0 && registerQuantity > maxQuantity) {
    throw new Error(`单帧最多读取 ${maxQuantity} 个位状态`)
  }

  return appendCrc16Modbus(
    [slave, command].concat(splitWord(startAddress), splitWord(registerQuantity)),
    MODBUS_CRC_OPTIONS
  )
}

function buildWriteSingleCoilFrame(slaveAddress, address, checked) {
  const slave = toByte(slaveAddress, '从站地址')
  const startAddress = toWord(address, '线圈地址')
  const outputValue = checked ? 0xFF00 : 0x0000

  return appendCrc16Modbus(
    [slave, 0x05].concat(splitWord(startAddress), splitWord(outputValue)),
    MODBUS_CRC_OPTIONS
  )
}

function buildWriteSingleRegisterFrame(slaveAddress, address, value) {
  const slave = toByte(slaveAddress, '从站地址')
  const startAddress = toWord(address, '寄存器地址')
  const registerValue = toWord(value, '写入值')

  return appendCrc16Modbus(
    [slave, 0x06].concat(splitWord(startAddress), splitWord(registerValue)),
    MODBUS_CRC_OPTIONS
  )
}

function buildWriteMultipleRegistersFrame(slaveAddress, address, values, options = {}) {
  const slave = toByte(slaveAddress, '从站地址')
  const startAddress = toWord(address, '寄存器地址')
  const maxQuantity = getMaxWriteMultipleRegisterQuantity(options.maxFrameBytes)

  if (!Array.isArray(values) || values.length === 0) {
    throw new Error('请输入至少一个寄存器写入值')
  }
  if (maxQuantity > 0 && values.length > maxQuantity) {
    throw new Error(`单帧最多写入 ${maxQuantity} 个寄存器`)
  }

  const registerBytes = values.reduce((result, value) => {
    return result.concat(splitWord(toWord(value, '写入值')))
  }, [])

  return appendCrc16Modbus(
    [slave, 0x10]
      .concat(splitWord(startAddress), splitWord(values.length), [registerBytes.length], registerBytes),
    MODBUS_CRC_OPTIONS
  )
}

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

function getReadResponseByteLength(functionCode, quantity) {
  if (functionCode === 0x01 || functionCode === 0x02) return MODBUS_READ_RESPONSE_OVERHEAD + Math.ceil(Number(quantity || 0) / 8)
  if (functionCode === 0x03 || functionCode === 0x04) return MODBUS_READ_RESPONSE_OVERHEAD + Number(quantity || 0) * 2

  return 0
}

function parseModbusResponse(bytes) {
  if (!Array.isArray(bytes) || bytes.length < 5 || !hasValidCrc16Modbus(bytes, MODBUS_CRC_OPTIONS)) return null

  const slaveAddress = bytes[0]
  const functionCode = bytes[1]

  if (functionCode & 0x80) {
    return {
      exceptionCode: bytes[2],
      functionCode,
      isException: true,
      protocol: PROTOCOL_NAME,
      slaveAddress,
      sourceFunctionCode: functionCode & 0x7F
    }
  }

  if (functionCode === 0x01 || functionCode === 0x02) {
    const byteCount = bytes[2]
    const dataEnd = 3 + byteCount
    if (bytes.length < dataEnd + 2) return null

    return {
      byteCount,
      dataBytes: bytes.slice(3, dataEnd),
      functionCode,
      isException: false,
      protocol: PROTOCOL_NAME,
      slaveAddress
    }
  }

  if (functionCode === 0x03 || functionCode === 0x04) {
    const byteCount = bytes[2]
    const dataEnd = 3 + byteCount
    if (bytes.length < dataEnd + 2) return null

    return {
      byteCount,
      dataBytes: bytes.slice(3, dataEnd),
      functionCode,
      isException: false,
      protocol: PROTOCOL_NAME,
      slaveAddress,
      words: bytesToWords(bytes.slice(3, dataEnd))
    }
  }

  if (functionCode === 0x05 || functionCode === 0x06 || functionCode === 0x10) {
    return {
      address: ((bytes[2] << 8) | bytes[3]) & 0xFFFF,
      functionCode,
      isException: false,
      protocol: PROTOCOL_NAME,
      quantityOrValue: ((bytes[4] << 8) | bytes[5]) & 0xFFFF,
      slaveAddress
    }
  }

  return {
    functionCode,
    isException: false,
    protocol: PROTOCOL_NAME,
    slaveAddress
  }
}

function parseModbusRequest(bytes) {
  if (!Array.isArray(bytes) || bytes.length < 4 || !hasValidCrc16Modbus(bytes, MODBUS_CRC_OPTIONS)) return null

  const slaveAddress = bytes[0]
  const functionCode = bytes[1]
  if (bytes.length < 6) return null

  const address = ((bytes[2] << 8) | bytes[3]) & 0xFFFF
  let quantity = 1
  let value

  if (functionCode === 0x01 || functionCode === 0x02 || functionCode === 0x03 || functionCode === 0x04 || functionCode === 0x10) {
    quantity = ((bytes[4] << 8) | bytes[5]) & 0xFFFF
  }
  if (functionCode === 0x05 || functionCode === 0x06) {
    value = ((bytes[4] << 8) | bytes[5]) & 0xFFFF
  }

  return {
    address,
    functionCode,
    kind: 'raw-hex',
    protocol: PROTOCOL_NAME,
    quantity,
    value,
    slaveAddress
  }
}

function getExpectedResponseLength(expected, responseFunctionCode, responseBytes = []) {
  if (!expected) return 0

  if (responseFunctionCode === (expected.functionCode | 0x80)) {
    return 5
  }

  if (responseFunctionCode === 0x01 || responseFunctionCode === 0x02) {
    if (responseBytes.length < 3) return 0

    return 3 + Number(responseBytes[2] || 0) + 2
  }

  if (responseFunctionCode === 0x03 || responseFunctionCode === 0x04) {
    if (responseBytes.length < 3) return 0

    return 3 + Number(responseBytes[2] || 0) + 2
  }

  if (responseFunctionCode === 0x05 || responseFunctionCode === 0x06 || responseFunctionCode === 0x10) {
    return 8
  }

  return 0
}

function isExpectedResponse(response, expected) {
  if (response.functionCode === 0x01 || response.functionCode === 0x02) {
    return Array.isArray(response.dataBytes) && response.dataBytes.length >= Math.ceil(expected.quantity / 8)
  }

  if (response.functionCode === 0x03 || response.functionCode === 0x04) {
    return Array.isArray(response.words) && response.words.length >= expected.quantity
  }

  if (response.functionCode === 0x10) {
    return response.address === expected.address && response.quantityOrValue === expected.quantity
  }

  if (response.functionCode === 0x05 || response.functionCode === 0x06) {
    if (response.address !== expected.address) return false
    if (Number.isInteger(expected.value)) return response.quantityOrValue === expected.value

    return true
  }

  return true
}

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

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

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

function getReadBufferHint(expected) {
  return expected ? getReadResponseByteLength(expected.functionCode, expected.quantity) : 0
}

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

  const expectedFunctionCodes = [expected.functionCode, expected.functionCode | 0x80]
  let matchIndex = -1

  for (let index = 0; index < buffer.length - 1; index += 1) {
    if (buffer[index] !== expected.slaveAddress) continue
    if (expectedFunctionCodes.indexOf(buffer[index + 1]) < 0) continue

    matchIndex = index
    break
  }

  if (matchIndex > 0) {
    buffer.splice(0, matchIndex)
  } else if (matchIndex < 0 && buffer.length > 2) {
    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 >= 2) {
    const responseFunctionCode = buffer[1]
    const responseLength = getExpectedResponseLength(expected, responseFunctionCode, 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 = parseModbusResponse(frameBytes)
    if (!response) {
      return {
        frameBytes,
        status: 'invalid'
      }
    }

    const responseCode = response.isException ? response.sourceFunctionCode : response.functionCode
    if (response.slaveAddress !== expected.slaveAddress || responseCode !== expected.functionCode) {
      buffer.shift()
      alignResponseBuffer(buffer, expected)
      continue
    }

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

    if (!isExpectedResponse(response, expected)) {
      return {
        response,
        status: 'mismatch'
      }
    }

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

  return {
    status: 'pending'
  }
}

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(functionCode, startAddress, quantity, options = {}) {
  const maxQuantity = getMaxReadQuantity(functionCode, options.maxFrameBytes)

  return splitQuantity(startAddress, quantity, maxQuantity || quantity)
}

const response = {
  MODBUS_EXCEPTION_MESSAGES,
  formatExceptionMessage,
  getExceptionText,
  getExpectedResponseLength,
  getReadBufferHint,
  isExpectedResponse,
  parseModbusRequest,
  parseModbusResponse,
  readResponseFromBuffer
}

module.exports = {
  MAX_MODBUS_DMA_BYTES,
  MAX_READ_COIL_QUANTITY,
  MAX_READ_REGISTER_QUANTITY,
  MAX_WRITE_MULTIPLE_REGISTER_QUANTITY,
  MODBUS_CRC_OPTIONS,
  MODBUS_EXCEPTION_MESSAGES,
  PROTOCOL_NAME,
  UNLIMITED_FRAME_BYTES,
  buildReadFrame,
  buildWriteMultipleRegistersFrame,
  buildWriteSingleCoilFrame,
  buildWriteSingleRegisterFrame,
  formatHex,
  getMaxReadQuantity,
  getMaxWriteMultipleRegisterQuantity,
  getReadChunks,
  getReadResponseByteLength,
  hasValidCrc16Modbus,
  response,
  splitQuantity
}
