const {
  formatFixedValue
} = require('../../utils/base-utils.js')

const TOKEN_NUMBER = 'number'
const TOKEN_IDENTIFIER = 'identifier'
const TOKEN_OPERATOR = 'operator'
const TOKEN_LEFT_PAREN = 'leftParen'
const TOKEN_RIGHT_PAREN = 'rightParen'
const ALLOWED_IDENTIFIERS = [
  'x',
  'value',
  'rawValue',
  'caveFreq',
  'refVolt',
  'ampGain',
  'rsShunt',
  'busDiv',
  'alongDiv',
  'maxPacketLength',
  'addressWidth',
  'storageAddressWidth'
]

function tokenizeFormula(formulaText) {
  const source = String(formulaText || '').trim()
  const tokens = []
  let index = 0

  while (index < source.length) {
    const char = source[index]

    if (/\s/.test(char)) {
      index += 1
      continue
    }

    if (/[0-9.]/.test(char)) {
      const rest = source.slice(index)
      const match = rest.match(/^(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/i)
      if (!match) throw new Error('公式数字无效')

      tokens.push({
        type: TOKEN_NUMBER,
        value: Number(match[0])
      })
      index += match[0].length
      continue
    }

    if (/[A-Za-z_]/.test(char)) {
      const rest = source.slice(index)
      const match = rest.match(/^[A-Za-z_][A-Za-z0-9_]*/)
      const name = match ? match[0] : ''
      if (ALLOWED_IDENTIFIERS.indexOf(name) < 0) {
        throw new Error(`公式变量 ${name || char} 不支持`)
      }

      tokens.push({
        type: TOKEN_IDENTIFIER,
        value: name
      })
      index += name.length
      continue
    }

    if ('+-*/'.indexOf(char) >= 0) {
      tokens.push({
        type: TOKEN_OPERATOR,
        value: char
      })
      index += 1
      continue
    }

    if (char === '(') {
      tokens.push({
        type: TOKEN_LEFT_PAREN,
        value: char
      })
      index += 1
      continue
    }

    if (char === ')') {
      tokens.push({
        type: TOKEN_RIGHT_PAREN,
        value: char
      })
      index += 1
      continue
    }

    throw new Error(`公式包含不支持字符 ${char}`)
  }

  return tokens
}

function getOperatorPrecedence(operator) {
  if (operator === 'u+' || operator === 'u-') return 3
  if (operator === '*' || operator === '/') return 2
  if (operator === '+' || operator === '-') return 1

  return 0
}

function operatorIsRightAssociative(operator) {
  return operator === 'u+' || operator === 'u-'
}

function toRpn(tokens) {
  const output = []
  const operators = []
  let previousToken = null

  tokens.forEach((token) => {
    if (token.type === TOKEN_NUMBER || token.type === TOKEN_IDENTIFIER) {
      output.push(token)
      previousToken = token
      return
    }

    if (token.type === TOKEN_OPERATOR) {
      const unary = !previousToken
        || previousToken.type === TOKEN_OPERATOR
        || previousToken.type === TOKEN_LEFT_PAREN
      const operator = unary && (token.value === '+' || token.value === '-')
        ? `u${token.value}`
        : token.value
      const precedence = getOperatorPrecedence(operator)

      while (operators.length) {
        const top = operators[operators.length - 1]
        if (top.type !== TOKEN_OPERATOR) break

        const topPrecedence = getOperatorPrecedence(top.value)
        if (
          topPrecedence > precedence
          || (topPrecedence === precedence && !operatorIsRightAssociative(operator))
        ) {
          output.push(operators.pop())
          continue
        }
        break
      }

      operators.push({
        type: TOKEN_OPERATOR,
        value: operator
      })
      previousToken = token
      return
    }

    if (token.type === TOKEN_LEFT_PAREN) {
      operators.push(token)
      previousToken = token
      return
    }

    if (token.type === TOKEN_RIGHT_PAREN) {
      let matched = false
      while (operators.length) {
        const top = operators.pop()
        if (top.type === TOKEN_LEFT_PAREN) {
          matched = true
          break
        }
        output.push(top)
      }

      if (!matched) throw new Error('公式括号不匹配')
      previousToken = token
    }
  })

  while (operators.length) {
    const operator = operators.pop()
    if (operator.type === TOKEN_LEFT_PAREN) throw new Error('公式括号不匹配')
    output.push(operator)
  }

  return output
}

function getContextValue(name, context) {
  if (name === 'x' || name === 'value' || name === 'rawValue') return context.x

  return context[name]
}

function evaluateRpn(rpn, context) {
  const stack = []

  rpn.forEach((token) => {
    if (token.type === TOKEN_NUMBER) {
      stack.push(token.value)
      return
    }

    if (token.type === TOKEN_IDENTIFIER) {
      const value = Number(getContextValue(token.value, context))
      if (!Number.isFinite(value)) throw new Error(`公式变量 ${token.value} 无效`)
      stack.push(value)
      return
    }

    if (token.type !== TOKEN_OPERATOR) return

    if (token.value === 'u+' || token.value === 'u-') {
      if (!stack.length) throw new Error('公式运算无效')
      const value = stack.pop()
      stack.push(token.value === 'u-' ? -value : value)
      return
    }

    if (stack.length < 2) throw new Error('公式运算无效')
    const right = stack.pop()
    const left = stack.pop()
    if (token.value === '+') stack.push(left + right)
    if (token.value === '-') stack.push(left - right)
    if (token.value === '*') stack.push(left * right)
    if (token.value === '/') stack.push(left / right)
  })

  if (stack.length !== 1 || !Number.isFinite(stack[0])) {
    throw new Error('公式计算结果无效')
  }

  return stack[0]
}

function formatFormulaNumber(value) {
  return formatFixedValue(value, 6).replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '')
}

function evaluateValueFormula(formulaText, rawValue, codeInfoContext = {}) {
  const formula = String(formulaText || '').trim()
  const numberValue = Number(rawValue)
  if (!formula || !Number.isFinite(numberValue)) {
    return {
      ok: false,
      value: numberValue,
      text: Number.isFinite(numberValue) ? formatFormulaNumber(numberValue) : '--'
    }
  }

  try {
    const context = {
      ...codeInfoContext,
      x: numberValue
    }
    const value = evaluateRpn(toRpn(tokenizeFormula(formula)), context)

    return {
      ok: true,
      value,
      text: formatFormulaNumber(value)
    }
  } catch (error) {
    return {
      errorText: error && error.message ? error.message : '公式无效',
      ok: false,
      value: numberValue,
      text: formatFormulaNumber(numberValue)
    }
  }
}

function validateValueFormula(formulaText) {
  const formula = String(formulaText || '').trim()
  if (!formula) return true

  toRpn(tokenizeFormula(formula))

  return true
}

module.exports = {
  ALLOWED_IDENTIFIERS,
  evaluateValueFormula,
  validateValueFormula
}
