// BLE protocol: frame encoding/decoding, checksum, constants

var PROTOCOL_MODE = 'vendor_33'

var SERVICE = {
  DEVICE_INFO: 'FFE0',
  DATA_COMM: 'FFE1',
  OTA: 'FFE2'
}

var CHAR = {
  DEVICE_INFO: 'FFE3',
  COMMAND: 'FFE1',
  STATUS: 'FFE4',
  BOND_INFO: 'FFE6',
  OTA_CONTROL: 'FFE7',
  OTA_DATA: 'FFE8',
  OTA_STATUS: 'FFE9'
}

var CMD = {
  SET_PARAMS: 0x01,
  START: 0x02,
  STOP: 0x03,
  QUERY_STATUS: 0x04,
  BIND: 0x05,
  UNBIND: 0x06
}

var NOTIFY = {
  STATUS_REPORT: 0x21,
  ACK: 0x22,
  TREATMENT_COMPLETE: 0x31,
  EXCEPTION: 0x32,
  BIND_SUCCESS: 0x33
}

var MODE_STATE = {
  IDLE: 0x00,
  SCANNING: 0x01,
  ACTIVE: 0x02,
  PAUSED: 0x03,
  COMPLETED: 0x04,
  ERROR: 0x05,
  OTA: 0x06
}

var WAVELENGTH = {
  IR: 1,
  R: 2,
  UV: 3,
  Y: 4
}

var TREAT_MODE = {
  NORMAL: 0,
  SMART: 1
}

var REGION = {
  LEFT_CHEEK: 0x01,
  RIGHT_CHEEK: 0x02,
  FOREHEAD: 0x04,
  CHIN: 0x08,
  NOSE: 0x10,
  LEFT_EYE: 0x20,
  RIGHT_EYE: 0x40,
  FULL_FACE: 0x7F
}

var REGION_NAMES = ['left_cheek', 'right_cheek', 'forehead', 'chin', 'nose', 'left_eye', 'right_eye']

var DEVICE_ERR = {
  0x00: 'SUCCESS',
  0x01: 'ERR_REGION_INVALID',
  0x02: 'ERR_REGION_EMPTY',
  0x03: 'ERR_BRIGHTNESS_INVALID',
  0x04: 'ERR_DURATION_INVALID',
  0x05: 'ERR_NOT_BOUND',
  0x06: 'ERR_NO_SUBSCRIPTION',
  0x07: 'ERR_TEMP_HIGH',
  0x08: 'ERR_BATTERY_LOW',
  0x09: 'ERR_ALREADY_RUNNING',
  0x0A: 'ERR_NOT_RUNNING',
  0x0B: 'ERR_OTA_FAILED',
  0x0C: 'ERR_BLE_DISCONNECTED'
}

// --- byte utilities ---

function bufferToBytes(buffer) {
  var arr = new Uint8Array(buffer)
  var bytes = []
  for (var i = 0; i < arr.length; i++) {
    bytes.push(arr[i])
  }
  return bytes
}

function bytesToBuffer(bytes) {
  var buffer = new ArrayBuffer(bytes.length)
  var view = new Uint8Array(buffer)
  for (var i = 0; i < bytes.length; i++) {
    view[i] = bytes[i]
  }
  return buffer
}

function xorChecksum(bytes) {
  var result = 0
  for (var i = 0; i < bytes.length; i++) {
    result ^= bytes[i]
  }
  return result
}

function clampByte(value) {
  var n = parseInt(value, 10)
  if (isNaN(n)) n = 0
  return Math.max(0, Math.min(255, n))
}

function clampUInt16(value) {
  var n = parseInt(value, 10)
  if (isNaN(n)) n = 0
  return Math.max(0, Math.min(65535, n))
}

function uint32ToBytes(value) {
  return [
    (value >> 24) & 0xFF,
    (value >> 16) & 0xFF,
    (value >> 8) & 0xFF,
    value & 0xFF
  ]
}

function bytesToUint32(bytes, offset) {
  return (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]
}

function hexToBytes(hex) {
  var bytes = []
  for (var i = 0; i < hex.length; i += 2) {
    bytes.push(parseInt(hex.substr(i, 2), 16))
  }
  return bytes
}

function bytesToHex(bytes) {
  var hex = ''
  for (var i = 0; i < bytes.length; i++) {
    hex += ('0' + bytes[i].toString(16)).slice(-2)
  }
  return hex.toUpperCase()
}

// --- vendor 34-byte command protocol ---
// Byte1-30: IO1~IO5 (6B each: R, IR, UV, Y, GainH, GainL)
// Byte31-32: hold time (seconds, big-endian)
// Byte33: control (0=IDLE, 1=scan, 2=treat)
// Byte34: XOR checksum of Byte1-33

var CONTROL = {
  IDLE: 0x00,
  SCAN: 0x01,
  TREAT: 0x02
}

function buildVendorCommand(options) {
  options = options || {}
  var regionMask = options.region_mask || REGION.FULL_FACE
  var wavelength = options.wavelength || WAVELENGTH.R
  var brightness = clampByte(options.brightness === undefined ? 200 : options.brightness)
  var gain = clampUInt16(options.current_gain === undefined ? 0 : options.current_gain)
  var durationMs = options.duration_ms || 600000
  var holdSeconds = clampUInt16(options.hold_time === undefined ? Math.round(durationMs / 1000) : options.hold_time)
  var control = options.control !== undefined ? options.control : CONTROL.TREAT
  var regions = [
    REGION.LEFT_CHEEK,
    REGION.RIGHT_CHEEK,
    REGION.FOREHEAD,
    REGION.CHIN,
    REGION.NOSE
  ]
  var bytes = []

  for (var i = 0; i < regions.length; i++) {
    var enabled = (regionMask & regions[i]) !== 0
    var red = 0
    var infrared = 0
    var uv = 0
    var warmYellow = 0
    if (enabled) {
      if (wavelength === WAVELENGTH.R) red = brightness
      if (wavelength === WAVELENGTH.IR) infrared = brightness
      if (wavelength === WAVELENGTH.UV) uv = brightness
      if (wavelength === WAVELENGTH.Y) warmYellow = brightness
    }
    bytes.push(red, infrared, uv, warmYellow, (gain >> 8) & 0xFF, gain & 0xFF)
  }

  bytes.push((holdSeconds >> 8) & 0xFF, holdSeconds & 0xFF)
  bytes.push(control & 0xFF)
  bytes.push(xorChecksum(bytes))
  return bytes
}

// deprecated: alias kept for backward compat, actually outputs 34 bytes
function buildVendorCommand33(options) {
  return buildVendorCommand(options)
}

function parseVendorStatus(buffer) {
  var bytes = bufferToBytes(buffer)
  if (bytes.length === 1) {
    return { type: 'heartbeat', heartbeat: bytes[0], is_heartbeat: true, raw_hex: bytesToHex(bytes) }
  }
  if (bytes.length === 19) {
    var checksum19 = xorChecksum(bytes.slice(0, 18))
    var pds19 = []
    for (var p = 0; p < 7; p++) {
      pds19.push(bytes[p * 2] | (bytes[p * 2 + 1] << 8))
    }
    return {
      type: 'adc',
      is_heartbeat: false,
      pd: pds19,
      vbat: bytes[14] | (bytes[15] << 8),
      fitting: bytes[16],
      run_state: bytes[17],
      checksum_ok: checksum19 === bytes[18],
      raw_hex: bytesToHex(bytes)
    }
  }
  if (bytes.length === 17) {
    var checksum = xorChecksum(bytes.slice(0, 16))
    var pds = []
    for (var p = 0; p < 7; p++) {
      pds.push(bytes[p * 2] | (bytes[p * 2 + 1] << 8))
    }
    return {
      type: 'adc',
      is_heartbeat: false,
      pd: pds,
      vbat: bytes[14] | (bytes[15] << 8),
      fitting: -1,
      run_state: -1,
      checksum_ok: checksum === bytes[16],
      raw_hex: bytesToHex(bytes)
    }
  }
  if (bytes.length === 34) {
    var ios34 = []
    for (var j = 0; j < 5; j++) {
      var off34 = j * 6
      ios34.push({
        red: bytes[off34], infrared: bytes[off34 + 1],
        uv: bytes[off34 + 2], warm_yellow: bytes[off34 + 3],
        gain: (bytes[off34 + 4] << 8) | bytes[off34 + 5]
      })
    }
    var expected34 = xorChecksum(bytes.slice(0, 33))
    return {
      type: 'params',
      is_heartbeat: false,
      ios: ios34,
      hold_time: (bytes[30] << 8) | bytes[31],
      control: bytes[32],
      checksum: bytes[33],
      checksum_ok: expected34 === bytes[33],
      raw_hex: bytesToHex(bytes)
    }
  }
  if (bytes.length === 33) {
    var ios = []
    for (var i = 0; i < 5; i++) {
      var off = i * 6
      ios.push({
        red: bytes[off], infrared: bytes[off + 1],
        uv: bytes[off + 2], warm_yellow: bytes[off + 3],
        gain: (bytes[off + 4] << 8) | bytes[off + 5]
      })
    }
    var expected33 = xorChecksum(bytes.slice(0, 32))
    return {
      type: 'params',
      is_heartbeat: false,
      ios: ios,
      hold_time: (bytes[30] << 8) | bytes[31],
      control: -1,
      checksum: bytes[32],
      checksum_ok: expected33 === bytes[32],
      raw_hex: bytesToHex(bytes)
    }
  }
  return { type: 'unknown', is_heartbeat: false, raw_bytes: bytes, raw_hex: bytesToHex(bytes) }
}

// --- frame encoding/decoding ---

function buildFrame(type, payload) {
  var len = payload ? payload.length : 0
  var frame = [0xAA, 0x55, len, type]
  if (payload && payload.length > 0) {
    frame = frame.concat(payload)
  }
  var checkBytes = frame.slice(0)
  frame.push(xorChecksum(checkBytes))
  return bytesToBuffer(frame)
}

function parseFrame(buffer) {
  var bytes = bufferToBytes(buffer)
  if (bytes.length < 5) return null
  if (bytes[0] !== 0xAA || bytes[1] !== 0x55) return null
  var len = bytes[2]
  if (bytes.length < 5 + len) return null
  var type = bytes[3]
  var payload = bytes.slice(4, 4 + len)
  var checksum = bytes[4 + len]
  var expected = xorChecksum(bytes.slice(0, 4 + len))
  if (checksum !== expected) return null
  return { type: type, payload: payload, seq: payload.length > 0 ? payload[payload.length - 1] : 0 }
}

// --- notification parsing ---

function parseStatusReport(payload) {
  if (payload.length < 14) return null
  return {
    mode_state: payload[0],
    region_mask: payload[1],
    wavelength: payload[2],
    brightness: payload[3],
    remaining_ms: bytesToUint32(payload, 4),
    error_code: payload[8],
    command_seq: payload[9],
    battery: payload[10],
    temperature: payload[11],
    bind_status: payload[12],
    subscription: payload[13]
  }
}

function parseAck(payload) {
  return {
    seq: payload[0],
    error_code: payload.length > 1 ? payload[1] : 0,
    error_msg: DEVICE_ERR[payload.length > 1 ? payload[1] : 0] || 'UNKNOWN'
  }
}

function parseTreatmentComplete(payload) {
  return {
    session_id: bytesToHex(payload.slice(0, 8)),
    regions: payload[8],
    total_duration_ms: bytesToUint32(payload, 9),
    avg_pd: payload[13]
  }
}

function parseException(payload) {
  return {
    error_code: payload[0],
    error_msg: DEVICE_ERR[payload[0]] || 'UNKNOWN',
    temperature: payload.length > 1 ? payload[1] : 0
  }
}

// --- display helpers ---

function getRegionName(mask) {
  var names = []
  var bits = [
    [0x01, '右区'], [0x02, '左区'], [0x04, '上区'],
    [0x08, '中区'], [0x10, '下区'], [0x20, '左上区'], [0x40, '右上区']
  ]
  for (var i = 0; i < bits.length; i++) {
    if (mask & bits[i][0]) names.push(bits[i][1])
  }
  return names
}

function getWavelengthName(code) {
  var map = { 1: '红外 850nm', 2: '红光 630nm', 3: '紫光 405nm', 4: '黄光 590nm' }
  return map[code] || '未知'
}

function getModeStateName(code) {
  var map = {
    0x00: '空闲', 0x01: '扫描中', 0x02: '使用中',
    0x03: '已暂停', 0x04: '已完成', 0x05: '异常', 0x06: 'OTA升级中'
  }
  return map[code] || '未知'
}

module.exports = {
  PROTOCOL_MODE: PROTOCOL_MODE,
  SERVICE: SERVICE,
  CHAR: CHAR,
  CMD: CMD,
  NOTIFY: NOTIFY,
  MODE_STATE: MODE_STATE,
  WAVELENGTH: WAVELENGTH,
  TREAT_MODE: TREAT_MODE,
  REGION: REGION,
  REGION_NAMES: REGION_NAMES,
  CONTROL: CONTROL,
  DEVICE_ERR: DEVICE_ERR,

  bufferToBytes: bufferToBytes,
  bytesToBuffer: bytesToBuffer,
  xorChecksum: xorChecksum,
  uint32ToBytes: uint32ToBytes,
  bytesToUint32: bytesToUint32,
  hexToBytes: hexToBytes,
  bytesToHex: bytesToHex,
  buildVendorCommand: buildVendorCommand,
  buildVendorCommand33: buildVendorCommand33,

  buildFrame: buildFrame,
  parseFrame: parseFrame,
  parseVendorStatus: parseVendorStatus,
  parseStatusReport: parseStatusReport,
  parseAck: parseAck,
  parseTreatmentComplete: parseTreatmentComplete,
  parseException: parseException,

  getRegionName: getRegionName,
  getWavelengthName: getWavelengthName,
  getModeStateName: getModeStateName
}
