const path = require('path')

const t = require('@babel/types')
const babelTraverse = require('@babel/traverse').default

const generate = require('./generate')
const uniI18n = require('@dcloudio/uni-cli-i18n')

const {
  genCode,
  getCode,
  getForKey,
  traverseKey,
  isComponent
} = require('../util')

const {
  ATTR_DATA_CUSTOM_HIDDEN,
  ATTR_SLOT_ORIGIN
} = require('../constants')

module.exports = function traverse (ast, state = {}) {
  babelTraverse(ast, {
    WithStatement (path) {
      state.ast = traverseExpr(path.node.body.body[0].argument, state)
    }
  })
  initParent(state.ast)
  return state.ast
}

function initParent (ast, parentNode) {
  if (Array.isArray(ast)) {
    ast.forEach(node => initParent(node, parentNode))
  } else if (typeof ast === 'object') {
    ast.parent = parentNode

    const vueId = ast.$vueId
    if (vueId) {
      const vuePid = getVueParentId(parentNode)
      if (vuePid) {
        ast.attr['vue-id'] = genCode(
          t.binaryExpression(
            '+',
            t.binaryExpression(
              '+',
              t.parenthesizedExpression(vueId),
              t.stringLiteral(',')
            ),
            t.parenthesizedExpression(vuePid)
          )
        )
      }
    }
    initParent(ast.children, ast)
  }
}

function getVueParentId (parentNode) {
  if (!parentNode) {
    return
  }
  return parentNode.$vueId || getVueParentId(parentNode.parent)
}

function traverseExpr (exprNode, state) {
  if (t.isCallExpression(exprNode)) {
    return traverseCallExpr(exprNode, state)
  } else if (t.isConditionalExpression(exprNode)) {
    return traverseConditionalExpr(exprNode, state)
  } else if (t.isArrayExpression(exprNode)) {
    return traverseArrayExpression(exprNode, state)
  } else if (t.isIdentifier(exprNode) && exprNode.name === 'undefined') {
    return {
      type: 'block',
      attr: {},
      children: []
    }
  } else if (t.isUnaryExpression(exprNode) && exprNode.operator === 'void') {
    return false
  } else {
    throw new Error(`暂不支持 ${getCode(exprNode)} 语法`)
  }
}

const traverses = {
  _c: traverseCreateElement,
  _t: traverseRenderSlot,
  _l: traverseRenderList,
  _u: traverseResolveScopedSlots,
  _v: traverseCreateTextVNode,
  _e: traverseCreateEmptyVNode,
  _g: '暂不支持 v-on="$listeners" 用法',
  _b: '暂不支持 v-bind="" 用法'
}

function traverseCallExpr (callExprNode, state) {
  const traverse = traverses[callExprNode.callee.name]
  if (!traverse) {
    throw new Error(
      `CallExpression ${callExprNode.callee.name}  is not yet implemented`
    )
  } else if (typeof traverse === 'string') {
    throw new Error(traverse)
  }

  return traverse(callExprNode, state)
}

function traverseConditionalExpr (conditionalExprNode, state) {
  const prefix = state.options.platform.directive
  const ret = [{
    type: 'block',
    attr: {
      [prefix + 'if']: genCode(conditionalExprNode.test)
    },
    children: normalizeChildren(
      traverseExpr(conditionalExprNode.consequent, state)
    )
  }]
  if (
    !(
      (t.isCallExpression(conditionalExprNode.alternate) &&
        t.isIdentifier(conditionalExprNode.alternate.callee) &&
        conditionalExprNode.alternate.callee.name === '_e') || t.isNullLiteral(conditionalExprNode.alternate)
    )
  ) {
    // test?_c():_e()
    ret.push({
      type: 'block',
      attr: {
        [prefix + 'else']: ''
      },
      children: normalizeChildren(
        traverseExpr(conditionalExprNode.alternate, state)
      )
    })
  }
  return ret
}

function traverseCreateElement (callExprNode, state) {
  const args = callExprNode.arguments
  const tagNode = args[0]
  if (!t.isStringLiteral(tagNode)) {
    throw new Error(`暂不支持动态组件[${tagNode.name}]`)
  }

  const node = {
    type: tagNode.value,
    attr: {},
    children: []
  }

  if (args.length < 2) {
    return node
  }

  const dataNodeOrChildNodes = args[1]
  if (t.isObjectExpression(dataNodeOrChildNodes)) {
    Object.assign(node.attr, traverseDataNode(dataNodeOrChildNodes, state, node))
  } else {
    node.children = normalizeChildren(traverseExpr(dataNodeOrChildNodes, state))
  }
  if (args.length < 3) {
    return node
  }
  const childNodes = args[2]
  if (!t.isNumericLiteral(childNodes)) {
    if (node.children && node.children.length) {
      node.children = node.children.concat(normalizeChildren(traverseExpr(childNodes, state)))
    } else {
      node.children = normalizeChildren(traverseExpr(childNodes, state))
    }
  }
  return node
}

function traverseDataNode (dataNode, state, node) {
  const ret = {}
  const specialEvents = state.options.platform.specialEvents[node.type] || {}
  const specialEventNames = Object.keys(specialEvents)
  dataNode.properties.forEach(property => {
    switch (property.key.name) {
      case 'slot':
        ret.slot = genCode(property.value)
        break
      case 'scopedSlots': // Vue 2.6
        property.value.$node = node
        node.children = normalizeChildren(traverseExpr(property.value, state))
        break
      case 'attrs':
      case 'domProps':
      case 'on':
      case 'nativeOn':
        property.value.properties.forEach(attrProperty => {
          if (attrProperty.key.value === 'vue-id') { // initParent 时再处理 vue-id
            node.$vueId = attrProperty.value
            ret[attrProperty.key.value] = genCode(attrProperty.value)
          } else {
            if (specialEventNames.includes(attrProperty.key.value)) {
              if (t.isIdentifier(attrProperty.value)) {
                ret[specialEvents[attrProperty.key.value]] = attrProperty.value.name
              }
            } else {
              ret[attrProperty.key.value] = genCode(attrProperty.value)
            }
          }
        })
        break
      case 'class':
      case 'staticClass':
        // vue@2.7.0 https://github.com/vuejs/vue/pull/12195 已经修复这个问题(question/184192)，后续升级vue版本后可以删除
        if (property.key.name === 'staticClass' && property.value.value) {
          property.value.value = property.value.value.replace(/\s+/g, ' ').trim()
        }
        ret.class = genCode(property.value)
        break
      case 'style':
      case 'staticStyle':
        ret.style = genCode(property.value)
        break
      case 'directives':
        property.value.elements.find(objectExpression => {
          if (t.isObjectExpression(objectExpression)) {
            const nameProperty = objectExpression.properties[0]
            const isShowDir =
              nameProperty &&
              nameProperty.key.name === 'name' &&
              t.isStringLiteral(nameProperty.value) &&
              nameProperty.value.value === 'show'
            if (isShowDir) {
              objectExpression.properties.find(valueProperty => {
                const isValue = valueProperty.key.name === 'value'
                if (isValue) {
                  let key
                  // 自定义组件不支持 hidden 属性
                  const platform = state.options.platform.name
                  const platforms = ['mp-weixin', 'mp-qq', 'mp-jd', 'mp-xhs', 'mp-toutiao', 'mp-lark']
                  if (isComponent(node.type) && platforms.includes(platform)) {
                    // 字节跳动|飞书小程序自定义属性不会反应在DOM上，只能使用事件格式
                    key = `${platform === 'mp-toutiao' || platform === 'mp-lark' ? 'bind:-' : ''}${ATTR_DATA_CUSTOM_HIDDEN}`
                  } else {
                    key = 'hidden'
                  }
                  ret[key] = genCode(valueProperty.value, false, true)
                }
                return isValue
              })
            }
            return isShowDir
          }
        })
        break
    }
  })
  return ret
}

function normalizeChildren (nodes) {
  if (!Array.isArray(nodes)) {
    nodes = [nodes]
  }
  return nodes.filter(node => {
    if (typeof node === 'string' && !node.trim()) {
      return false
    }
    return true
  })
}

function traverseArrayExpression (arrayExprNodes, state) {
  return arrayExprNodes.elements.reduce((nodes, exprNode) => {
    return nodes.concat(traverseExpr(exprNode, state))
  }, [])
}

function genSlotNode (slotName, slotNode, fallbackNodes, state, isStaticSlotName = true) {
  if (!fallbackNodes || t.isNullLiteral(fallbackNodes)) {
    return slotNode
  }
  // 支付宝小程序默认插槽为 $default
  if (state.options.platform.name === 'mp-alipay') {
    slotName = slotName === 'default' ? '$default' : slotName
  }
  const prefix = state.options.platform.directive
  return [{
    type: 'block',
    attr: {
      // 移除动态拼接的 index 部分
      [prefix + 'if']: isStaticSlotName ? '{{$slots.' + slotName + '}}' : '{{$slots[' + slotName.replace(/^{{/, '').replace(/}}$/, '').replace(/\+\('\.'\+\S+?\)$/, '') + ']}}'
    },
    children: [].concat(slotNode)
  }, {
    type: 'block',
    attr: {
      [prefix + 'else']: ''
    },
    children: normalizeChildren(
      traverseExpr(fallbackNodes, state)
    )
  }]
}

function traverseRenderSlot (callExprNode, state) {
  const slotNameNode = callExprNode.arguments[0]
  const isStaticSlotName = t.isStringLiteral(slotNameNode)
  const slotName = isStaticSlotName ? slotNameNode.value : genCode(slotNameNode)

  let deleteSlotName = false // 标记是否组件 slot 手动指定了 name="default"
  if (state.options.scopedSlotsCompiler !== 'augmented' && callExprNode.arguments.length > 2) { // 作用域插槽
    const props = {}
    const arg2 = callExprNode.arguments[2]
    const arg3 = callExprNode.arguments[3]
    let bindings
    if (t.isObjectExpression(arg2)) {
      arg2.properties.forEach(property => {
        props[property.key.value] = genCode(property.value)
      })
    } else if (arg3) {
      bindings = genCode(arg3)
    }
    deleteSlotName = props.SLOT_DEFAULT && Object.keys(props).length === 1
    if (!deleteSlotName) {
      // TODO 非原生支持作用域插槽的平台在未启用增强的模式下也允许使用动态插槽名
      if (!isStaticSlotName && !['mp-baidu', 'mp-alipay'].includes(state.options.platform.name)) {
        state.errors.add(uniI18n.__('templateCompiler.notSupportDynamicSlotName', { 0: 'v-slot' }))
        return
      }
      delete props.SLOT_DEFAULT
      return genSlotNode(
        slotName,
        state.options.platform.createScopedSlots(slotName, bindings || props, state),
        callExprNode.arguments[1],
        state
      )
    }
  }

  const node = {
    type: 'slot',
    attr: {
      name: slotName
    },
    children: []
  }

  if (deleteSlotName) {
    delete node.attr.name
  }

  return genSlotNode(slotName, node, callExprNode.arguments[1], state, isStaticSlotName)
}

function traverseResolveScopedSlots (callExprNode, state) {
  const options = state.options
  const prefix = options.platform.directive
  const platformName = options.platform.name
  const vIfAttrName = prefix + 'if'
  const vForAttrName = prefix + 'for'
  // 模板标签支持 slot 属性的平台
  // 百度、字节小程序仅支持在根节点使用 slot 属性
  const supportTemplateSlotPlatforms = ['mp-baidu', 'mp-toutiao']
  // 支持访问当前节点 v-for 作用域的平台
  const supportCurrentScopePlatforms = ['mp-weixin', 'mp-alipay']
  function merge (node, ignore, vIfs = [], top, needRealNode) {
    if (!top) {
      // 支付宝小程序使用静态插槽时可以在非实体节点使用 slot 属性，其他小程序 named slot 需移动到实体节点
      const slot = node.attr.slot
      needRealNode = slot && slot !== 'default' && !supportTemplateSlotPlatforms.includes(platformName) && !(platformName === 'mp-alipay' && !/\{\{.+?\}\}/.test(slot))
      node = { children: [node] }
      top = node
    }
    let children = node.children
    let nodeAttr = node.attr || {}
    function resolveVIf () {
      if (vIfs.length) {
        // 简易合并
        nodeAttr[vIfAttrName] = vIfs.length > 1 ? `{{${vIfs.map(str => str.replace(/^\{\{(.+)\}\}$/, '($1)')).join('&&')}}}` : vIfs[0]
        vIfs.length = 0
      }
    }
    if (Array.isArray(children)) {
      children = children.filter(child => !!child)
      let slotNode
      if (children.length === 1) {
        let child = children[0]
        if (child.type) {
          const attr = child.attr || {}
          // 除 v-if 外与父节点无同名属性且当前节点无 v-for 作用域且父节点 v-for 支持访问当前节点作用域，向上合并
          // TODO 父节点访问变量不与当前 v-for 作用域内变量同名时，可向上合并
          if (!Object.keys(attr).find(key => key !== vIfAttrName && key in nodeAttr) && !attr[vForAttrName] && (supportCurrentScopePlatforms.includes(platformName) || !nodeAttr[vForAttrName])) {
            if (attr[vIfAttrName]) {
              vIfs.push(attr[vIfAttrName])
              delete attr[vIfAttrName]
            }
            child.attr = nodeAttr = Object.assign(attr, nodeAttr)
            for (const key in child) {
              node[key] = child[key]
            }
            child = node
          } else {
            resolveVIf()
          }
          if (ignore.includes(child.type)) {
            return merge(child, ignore, vIfs, top, needRealNode)
          } else if (needRealNode) {
            slotNode = child
          }
        } else if (needRealNode) {
          node.type = 'text'
          slotNode = node
        }
      } else if (needRealNode) {
        // TODO 依据子节点类型
        node.type = 'view'
        slotNode = node
      }
      if (slotNode && slotNode !== top) {
        // TODO 多层 v-for 嵌套时，此处理导致作用域发生变化，需安全重命名 slot name
        ['slot', 'slot-scope'].forEach(key => {
          const topAttr = top.attr
          if (key in topAttr) {
            slotNode.attr[key] = topAttr[key]
            delete topAttr[key]
          }
        })
      }
    }
    resolveVIf()
    return top
  }
  return callExprNode.arguments[0].elements.map(slotNode => {
    let keyProperty = false
    let fnProperty = false
    let proxyProperty = false
    let vIfNode
    let vForNode
    // TODO v-else
    if (t.isConditionalExpression(slotNode)) {
      // vIfCode = genCode(slotNode.test)
      vIfNode = t.cloneNode(slotNode, true)
      slotNode = slotNode.consequent
    }
    if (t.isCallExpression(slotNode)) {
      vForNode = t.cloneNode(slotNode, true)
      slotNode = slotNode.arguments[1].body.body[0].argument
    }
    slotNode.properties.forEach(property => {
      switch (property.key.name) {
        case 'key':
          keyProperty = property
          break
        case 'fn':
          fnProperty = property
          break
        case 'proxy':
          proxyProperty = property
      }
    })
    const slotNameNode = keyProperty.value
    const isStaticSlotName = t.isStringLiteral(slotNameNode)
    const slotName = isStaticSlotName ? slotNameNode.value : genCode(slotNameNode)
    // 移除动态拼接的 index 部分
    // TODO 动态 slotName 如使用到 v-for 作用域变量，输出固定名称 $dynamic
    const slotNameOrigin = isStaticSlotName ? slotName : slotName.replace(/\+\('\.'\+\S+?\)\}\}$/, '}}')
    let returnExprNodes = fnProperty.value.body.body[0].argument
    if (vForNode) {
      vForNode.arguments[1].body.body[0].argument = returnExprNodes
      returnExprNodes = vForNode
    }
    if (vIfNode) {
      vIfNode.consequent = returnExprNodes
      returnExprNodes = vIfNode
    }
    const parentNode = callExprNode.$node
    if (options.scopedSlotsCompiler !== 'augmented' && slotNode.scopedSlotsCompiler !== 'augmented' && !proxyProperty) {
      // 暂不处理旧版编译模式对于动态 slotName 的处理
      const resourcePath = options.resourcePath
      const ownerName = path.basename(resourcePath, path.extname(resourcePath))

      const parentName = parentNode.type

      const paramExprNode = fnProperty.value.params[0]
      const node = options.platform.resolveScopedSlots(
        slotName, {
          genCode,
          generate,
          ownerName,
          parentName,
          parentNode,
          resourcePath,
          paramExprNode,
          returnExprNodes,
          traverseExpr: function (exprNode, state) {
            const ast = traverseExpr(exprNode, state)
            initParent(ast)
            return ast
          },
          normalizeChildren
        },
        state
      )
      // 对原生支持作用域插槽的小程序平台，优化节点
      if (['mp-baidu', 'mp-alipay'].includes(platformName)) {
        node.attr[ATTR_SLOT_ORIGIN] = slotNameOrigin
        return merge(node, ['template', 'block'])
      }
      return node
    }
    if (options.scopedSlotsCompiler === 'auto' && slotNode.scopedSlotsCompiler === 'augmented') {
      parentNode.attr['scoped-slots-compiler'] = 'augmented'
    }
    // 除百度、字节外其他小程序仅默认插槽可以支持多个节点
    return merge({
      type: 'block',
      children: normalizeChildren(traverseExpr(returnExprNodes, state)),
      attr: {
        slot: slotName,
        [ATTR_SLOT_ORIGIN]: slotNameOrigin
      }
    }, ['template', 'block'])
  })
}

function traverseRenderList (callExprNode, state) {
  const params = callExprNode.arguments[1].params
  const forItem = params.length > 0 ? params[0].name : 'item'
  const forIndex = params.length > 1 ? params[1].name : ''

  const forReturnStatementArgument =
    callExprNode.arguments[1].body.body[0].argument

  const forKey = traverseKey(forReturnStatementArgument, state)

  const prefix = state.options.platform.directive

  const isBaidu = state.options.platform.name === 'mp-baidu'
  let forValue = genCode(callExprNode.arguments[0], isBaidu)

  if (isBaidu && forKey) {
    forValue += ` trackBy ${getForKey(forKey, forIndex, state)}`
  }

  const attr = {
    [prefix + 'for']: forValue,
    [prefix + 'for-item']: forItem
  }

  if (forIndex) {
    attr[prefix + 'for-index'] = forIndex
  }

  if (forKey && !isBaidu) {
    const key = getForKey(forKey, forIndex, state)
    if (key) {
      attr[prefix + 'key'] = key
    }
  }

  const children = traverseExpr(forReturnStatementArgument, state)
  // 支付宝小程序在 block 标签上使用 key 时顺序不能保障
  if (state.options.platform.name === 'mp-alipay' && t.isCallExpression(forReturnStatementArgument) && children &&
    children.type) {
    children.attr = children.attr || {}
    Object.assign(children.attr, attr)
    return children
  }
  return {
    type: 'block',
    attr,
    children: normalizeChildren(children)
  }
}

function getLeftStringLiteral (expr) {
  if (t.isBinaryExpression(expr) && !expr.$toString) {
    return getLeftStringLiteral(expr.left)
  } else if (t.isStringLiteral(expr)) {
    return expr
  }
}

function trim (text, type) {
  // TODO 保留换行符？
  if (type === 'left') {
    text = text.trimLeft()
  } else if (type === 'right') {
    text = text.trimRight()
  } else {
    text = text.trim()
  }
  return text
}

function traverseCreateTextVNode (callExprNode, state) {
  // trimStart|Left and trimEnd|End
  const arg = callExprNode.arguments[0]
  if (t.isStringLiteral(arg)) {
    arg.value = trim(arg.value)
  } else if (t.isBinaryExpression(arg) && !arg.$toString) { // 非_s()
    // right
    const right = arg.right
    if (t.isStringLiteral(right)) {
      right.value = trim(right.value, 'right')
    }
    // left
    const left = getLeftStringLiteral(arg.left)
    if (left && left.value) {
      left.value = trim(left.value, 'left')
    }
  }
  if (
    state.options.platform.name === 'mp-baidu' ||
    state.options.platform.name === 'mp-qq'
  ) {
    const code = genCode(arg, false, false, false)
    if (code.indexOf('{{') === 0) {
      if (state.options.platform.name === 'mp-qq') { // 似乎百度也可以走该逻辑, 为了稳定性，仅限 qq
        return code.replace(/\\n/g, '\\\\n').replace(/\\t/g, '\\\\t')
      }
      return code.replace(/([^\\])\\n/g, '$1\\\\n').replace(/([^\\])\\t/g, '$1\\\\t')
    }
    return code
  }
  return genCode(arg, false, false, false).replace(/\\\\n/g, '\\n')
}

function traverseCreateEmptyVNode (callExprNode, state) {
  return ''
}
