const _ = require('lodash')
const { evaluate } = require('mathjs')

function generateAssociateKey (value) {
  const keys = Object.keys(value) || []
  return keys.length ? `${keys[0]}.${value[keys[0]]}` : null
}

function generateAssociateObjValue (str) {
  const splitValueString = str ? str.split('.') : []
  if (splitValueString.length !== 2) return null
  return ['@suggestion_saved_data', '@saved_data'].includes(splitValueString[0])
    ? splitValueString[1]
    : { [splitValueString[0]]: splitValueString[1] }
}

function getOperater (statement) {
  const _keys = Object.keys(statement) || []
  const _type = _keys[0]
  const eq = ['none', 'plus', 'minus', 'times', 'multiply', 'divide', 'divided_by', 'sum', 'avg', 'max', 'min', 'abs', 'count'].includes(_type)
  return eq ? _type : null
}

function generateField (value) {
  const data = {
    secondary: value.id
  }
  return data || {}
}

function parser (statement) {
  if (!statement) return []
  const keys = Object.keys(statement) || []
  const type = keys[0]
  const value = statement[type]
  let points = value.map((obj) => {
    const _keys = Object.keys(obj) || []
    const _type = _keys[0]
    let result
    if (typeof obj === 'number') {
      result = { type: 'number', value: obj }
    } else if (typeof obj === 'string') {
      result = { type: 'associate', value: `@saved_data.${obj}` }
    } else if (['plus', 'minus', 'times', 'multiply', 'divide', 'divided_by', 'sum', 'avg', 'max', 'min', 'abs', 'count'].includes(_type)) {
      result = parser(obj)
    } else if (_type === 'secondary') {
      result = ['sum', 'avg', 'min', 'max', 'count'].includes(obj.secondary)
        ? { type: 'field-aggregator', value: { id: obj.secondary } }
        : { type: 'field', value: { id: obj.secondary } }
    } else {
      result = { type: 'associate', value: generateAssociateKey(obj) }
    }
    return result
  })
  const _points = []
  if (['plus', 'minus', 'times', 'multiply', 'divide', 'divided_by'].includes(type)) {
    _points.unshift({ type: 'begin-bracket', value: type })
    points.forEach((item, index) => {
      _points.push(item)
      if ((points.length - 1) !== index) {
        _points.push({ type: 'operator', value: type })
      }
    })
    _points.push({ type: 'end-bracket', value: type })
    points = _.cloneDeep(_points)
  } else if (['sum', 'avg', 'min', 'max', 'count'].includes(type)) {
    points.unshift({ type: 'begin-aggregator', value: type })
    points.push({ type: 'end-aggregator', value: type })
  }
  return points.flat(1)
}

function Calculator (equation) {
  if (!(this instanceof Calculator)) {
    return new Calculator(equation)
  }

  const calculator = this
  if (_.isArray(equation)) {
    calculator.points = equation
  } else if (_.isObject(equation)) {
    calculator.points = parser(equation)
  } else {
    calculator.points = []
  }
}

function toSeries () {
  const calculator = this
  const series = []
  const stacks = []
  calculator.points.forEach((obj, index) => {
    if (['number', 'field', 'field-aggregator', 'associate', 'operator'].includes(obj.type)) {
      series.push({
        ...obj,
        id: index
      })
    } else if (['begin-aggregator', 'begin-bracket'].includes(obj.type)) {
      series.push({
        ...obj,
        id: index
      })
      stacks.push(index)
    } else if (['end-aggregator', 'end-bracket'].includes(obj.type)) {
      const id = stacks.pop()
      series.push({
        ...obj,
        id
      })
    } else if (['aggregator'].includes(obj.type)) {
      series.unshift({
        ...obj,
        type: 'begin-aggregator',
        id: index
      })
      series.push({
        ...obj,
        type: 'end-aggregator',
        id: index
      })
    }
  })
  return series
}

function toJSON () {
  const calculator = this
  const series = calculator.toSeries()

  let statement = { none: [] }
  const temps = []
  let bracket = {}
  let currentBracket = _.last(temps)
  let _statement = _.cloneDeep(currentBracket ? bracket[currentBracket] : statement)
  series.forEach(({ type, value }, index) => {
    currentBracket = _.last(temps)
    _statement = _.cloneDeep(currentBracket ? bracket[currentBracket] : statement)
    if (['number'].includes(type)) {
      const _type = getOperater(_statement)
      _statement[_type].push(Number(value))
    } else if (['field', 'field-aggregator'].includes(type)) {
      const _type = getOperater(_statement)
      _statement[_type].push(generateField(value))
    } else if (['associate'].includes(type)) {
      const _type = getOperater(_statement)
      _statement[_type].push(generateAssociateObjValue(value))
    } else if (['operator'].includes(type)) {
      const _type = getOperater(_statement)
      if (_type !== 'none') {
        _statement = {
          [value]: [_statement]
        }
      } else {
        _statement = {
          [value]: _statement[_type]
        }
      }
    } else if (['begin-bracket'].includes(type)) {
      const _type = getOperater(_statement)
      const name = `@bracket-${index}`
      bracket = {
        ...bracket,
        [name]: { none: [] }
      }
      _statement[_type].push(name)
      temps.push(name)
    } else if (['begin-aggregator'].includes(type)) {
      let _type = getOperater(_statement)
      let name = `@${value}-${index}`
      bracket = {
        ...bracket,
        [name]: { none: [] }
      }
      _statement[_type].push(name)
      temps.push(name)

      if (currentBracket) {
        bracket[currentBracket] = _.cloneDeep(_statement)
      } else {
        statement = _.cloneDeep(_statement)
      }

      currentBracket = _.last(temps)
      _statement = _.cloneDeep(currentBracket ? bracket[currentBracket] : statement)
      _type = getOperater(_statement)
      if (_type !== 'none') {
        _statement = {
          [value]: [_statement]
        }
      } else {
        _statement = {
          [value]: _statement[_type]
        }
      }

      if (currentBracket) {
        bracket[currentBracket] = _.cloneDeep(_statement)
      } else {
        statement = _.cloneDeep(_statement)
      }

      currentBracket = _.last(temps)
      _statement = _.cloneDeep(currentBracket ? bracket[currentBracket] : statement)
      _type = getOperater(_statement)
      name = `@bracket-${index}`
      bracket = {
        ...bracket,
        [name]: { none: [] }
      }
      _statement[_type].push(name)
      temps.push(name)
    } else if (['end-bracket'].includes(type)) {
      const temp = temps.pop()
      currentBracket = _.last(temps)
      _statement = _.cloneDeep(currentBracket ? bracket[currentBracket] : statement)
      const _type = getOperater(_statement)
      _statement[_type].pop()
      const _tempType = getOperater(bracket[temp])
      const _sm = _tempType !== 'none' ? bracket[temp] : bracket[temp].none[0]
      if (_type !== 'none') {
        _statement[_type].push(_sm)
      } else {
        _statement = _sm
      }
    } else if (['end-aggregator'].includes(type)) {
      let temp = temps.pop()
      currentBracket = _.last(temps)
      _statement = _.cloneDeep(currentBracket ? bracket[currentBracket] : statement)
      let _type = getOperater(_statement)
      _statement[_type].pop()
      let _tempType = getOperater(bracket[temp])
      let _sm = _tempType !== 'none' ? bracket[temp] : bracket[temp].none[0]
      if (_type !== 'none') {
        _statement[_type].push(_sm)
      } else {
        _statement = _.cloneDeep(_sm)
      }

      if (currentBracket) {
        bracket[currentBracket] = _.cloneDeep(_statement)
      } else {
        statement = _.cloneDeep(_statement)
      }

      temp = temps.pop()
      currentBracket = _.last(temps)
      _statement = _.cloneDeep(currentBracket ? bracket[currentBracket] : statement)
      _type = getOperater(_statement)
      _statement[_type].pop()
      _tempType = getOperater(bracket[temp])
      _sm = _tempType !== 'none' ? bracket[temp] : bracket[temp].none[0]
      if (_type !== 'none') {
        _statement[_type].push(_sm)
      } else {
        _statement = _.cloneDeep(_sm)
      }
    }
    if (currentBracket) {
      bracket[currentBracket] = _.cloneDeep(_statement)
    } else {
      statement = _.cloneDeep(_statement)
    }
  })
  return statement
}

function toValue (variable = {}) {
  const calculator = this
  const series = calculator.toSeries()
  let str = ''
  series.forEach((item) => {
    if (item.type === 'associate') {
      str += variable[item.value] || 0
    } else if (item.type === 'begin-aggregator') {
      str += `${item.value}(`
    } else if (item.type === 'operator') {
      switch (item.value) {
      case 'plus':
        str += ' + '
        break
      case 'minus':
        str += ' - '
        break
      case 'times':
      case 'multiply':
        str += ' * '
        break
      default:
        str += ' / '
        break
      }
    } else if (['begin-bracket', 'end-bracket'].includes(item.type)) {
      str += ''
    } else if (['end-aggregator'].includes(item.type)) {
      str += ')'
    } else {
      str += item.value
    }
  })
  return evaluate(str)
}
Calculator.prototype = { toSeries, toJSON, toValue }
Calculator.parser = parser

module.exports = Calculator
