const {
  normalizeTypeText,
  parseDeclarator,
  parseFirstDeclarator,
  resolveType,
  splitDeclarations,
  splitDeclarators
} = require('./struct-c-syntax.js')

const FIELD_TYPE_QUALIFIERS = {
  _I: true,
  _IO: true,
  _O: true,
  code: true,
  const: true,
  data: true,
  extern: true,
  idata: true,
  pdata: true,
  register: true,
  static: true,
  volatile: true,
  xdata: true
}

function isAsciiArray(typeText, dataType, name, arrayLength) {
  if (!arrayLength || arrayLength < 2 || arrayLength > 32) return false

  const normalizedType = normalizeTypeText(typeText).toLowerCase()
  if (normalizedType === 'char' || normalizedType === 'signed char') return true

  return dataType === 'uint8_t' && /(^|_)(model|name|text|str|string|chip|version|ver|serial|sn)($|_)/i.test(name)
}

function getDataTypeByteLength(dataType) {
  if (dataType === 'float' || dataType === 'int32_t' || dataType === 'uint32_t') return 4
  if (dataType === 'int8_t' || dataType === 'uint8_t') return 1

  return 2
}

function getBitFieldDataType(bitWidth) {
  const width = Math.max(1, Math.round(Number(bitWidth) || 1))
  if (width <= 8) return 'uint8_t'
  if (width <= 16) return 'uint16_t'

  return 'uint32_t'
}

function cloneEnumOptions(enumInfo) {
  return (Array.isArray(enumInfo && enumInfo.options) ? enumInfo.options : []).map((option) => ({
    label: option.label || option.name,
    name: option.name || option.label,
    value: Number(option.value) || 0
  }))
}

function createEnumMeta(enumInfo) {
  if (!enumInfo || !Array.isArray(enumInfo.options) || !enumInfo.options.length) return {}

  return {
    enumName: enumInfo.name,
    enumOptions: cloneEnumOptions(enumInfo),
    sourceSymbolType: enumInfo.name
  }
}

function normalizeEnumLookupKey(typeText) {
  return normalizeTypeText(typeText)
    .split(/\s+/)
    .filter((token) => !FIELD_TYPE_QUALIFIERS[token])
    .join(' ')
    .trim()
    .toLowerCase()
}

function resolveEnumInfo(typeText, enumTypes = {}) {
  const key = normalizeEnumLookupKey(typeText)
  if (!key) return null

  return enumTypes[key]
    || enumTypes[key.replace(/^enum\s+/, '')]
    || null
}

function isBitType(typeText) {
  return normalizeTypeText(typeText).toLowerCase() === 'bit'
}

function alignLayoutToByte(layoutState) {
  if (layoutState.bitOffset % 8 !== 0) {
    layoutState.bitOffset += 8 - (layoutState.bitOffset % 8)
  }
}

function getLayoutByteStart(layoutState) {
  return Math.floor(layoutState.bitOffset / 8)
}

function advanceLayoutBytes(layoutState, byteLength) {
  layoutState.bitOffset += Math.max(1, Number(byteLength) || 1) * 8
}

function createBitFieldRegister(field, bitWidth, layoutState, name, enumInfo = null) {
  const width = Math.max(0, Math.round(Number(bitWidth) || 0))

  if (width === 0) {
    alignLayoutToByte(layoutState)
    return []
  }

  const byteStart = getLayoutByteStart(layoutState)
  const bitOffset = layoutState.bitOffset % 8
  layoutState.bitOffset += width

  if (!name) return []

  return [{
    bitOffset,
    bitWidth: width,
    byteStart,
    dataType: getBitFieldDataType(width),
    ...createEnumMeta(enumInfo),
    isBitField: true,
    name,
    unit: 'bit'
  }]
}

function createRegisterFromField(field, dataType, originalTypeText, layoutState, enumInfo = null) {
  const arrayLength = field.arrayDimensions.reduce((total, value) => total * value, 1)
  const hasArray = field.arrayDimensions.length > 0
  const bitFieldWidth = field.bitWidth !== null && field.bitWidth !== undefined
    ? field.bitWidth
    : (isBitType(originalTypeText) ? 1 : null)

  if (bitFieldWidth !== null && bitFieldWidth !== undefined) {
    if (hasArray) {
      const registers = []
      for (let index = 0; index < arrayLength; index += 1) {
        registers.push(...createBitFieldRegister(
          field,
          bitFieldWidth,
          layoutState,
          field.name ? `${field.name}[${index}]` : '',
          enumInfo
        ))
      }

      return registers
    }

    return createBitFieldRegister(field, bitFieldWidth, layoutState, field.name, enumInfo)
  }

  alignLayoutToByte(layoutState)

  if (hasArray && isAsciiArray(originalTypeText, dataType, field.name, arrayLength)) {
    const byteStart = getLayoutByteStart(layoutState)
    advanceLayoutBytes(layoutState, arrayLength)

    return [{
      byteStart,
      dataType: 'ascii',
      ...createEnumMeta(enumInfo),
      name: field.name,
      textByteLength: String(arrayLength)
    }]
  }

  if (!hasArray) {
    const byteStart = getLayoutByteStart(layoutState)
    advanceLayoutBytes(layoutState, getDataTypeByteLength(dataType))

    return [{
      byteStart,
      dataType,
      ...createEnumMeta(enumInfo),
      name: field.name
    }]
  }

  const registers = []
  for (let index = 0; index < arrayLength; index += 1) {
    const byteStart = getLayoutByteStart(layoutState)
    advanceLayoutBytes(layoutState, getDataTypeByteLength(dataType))
    registers.push({
      byteStart,
      dataType,
      ...createEnumMeta(enumInfo),
      name: `${field.name}[${index}]`
    })
  }

  return registers
}

function parseStructFields(body, aliases, enumTypes = {}) {
  const registers = []
  const layoutState = {
    bitOffset: 0
  }
  const declarations = splitDeclarations(body)

  declarations.forEach((statement) => {
    if (!statement || statement.indexOf('(') >= 0) return

    const parts = splitDeclarators(statement)
    if (!parts.length) return

    const first = parseFirstDeclarator(parts[0])
    if (!first) return

    const dataType = resolveType(first.typeText, aliases)
    if (!dataType) return
    const enumInfo = resolveEnumInfo(first.typeText, enumTypes)

    const declarators = [first.declarator].concat(parts.slice(1))
    declarators.forEach((declaratorText) => {
      const field = parseDeclarator(declaratorText)
      if (!field) return

      registers.push(...createRegisterFromField(field, dataType, first.typeText, layoutState, enumInfo))
    })
  })

  return registers.map((register) => ({
    ...register,
    structByteLength: Math.ceil(layoutState.bitOffset / 8)
  }))
}

module.exports = {
  parseStructFields
}
