import { isComponent } from '../util/is'
import { remove } from '../util/shared'

export default function diff(vm, vnode, newVNode) {
  if (!vnode) {
    newVNode.createElement()
    return newVNode
  }

  if (!sameVnode(vnode, newVNode)) {
    vnode.destroy()
    vm.$$vnode.insertBefore(newVNode)
    return newVNode
  }

  return patchVNode(vnode, newVNode)
}

function diffChildren(parent, newVnode) {
  const ch = Array.from(parent.children)
  const newCh = Array.from(newVnode.children)
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = ch.length - 1
  let newEndIdx = newCh.length - 1
  let oldStartNode = ch[oldStartIdx]
  let oldEndNode = ch[oldEndIdx]
  let newStartNode = newCh[newStartIdx]
  let newEndNode = newCh[newEndIdx]

  // 任意一组 start > end 的时候，代表某一组已经遍历完毕，无须再比对
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (sameVnode(oldStartNode, newStartNode)) {
      // 向后移动 startIndex
      patchVNode(oldStartNode, newStartNode)
      oldStartNode = ch[++oldStartIdx]
      newStartNode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndNode, newEndNode)) {
      // 向前移动 endIndex
      patchVNode(oldEndNode, newEndNode)
      oldEndNode = ch[++oldEndIdx]
      newEndNode = newCh[++newEndIdx]
    } else if (sameVnode(oldStartNode, newEndNode)) {
      // oldStart 移动到 oldEndNode 的位置
      patchVNode(oldStartNode, newEndNode)
      parent.insertBefore(oldStartNode, getNodeSibling(parent.children, oldEndNode))
      oldStartNode = ch[++oldStartIdx]
      newEndNode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndNode, newStartNode)) {
      // oldEnd 移到 oldStart 前面
      patchVNode(oldEndNode, newStartNode)
      parent.insertBefore(oldEndNode, oldStartNode)
      oldEndNode = ch[--oldEndIdx]
      newStartNode = newCh[++newStartIdx]
    } else {
      // create new Dom
      newStartNode.createElement()
      parent.insertBefore(newStartNode, oldStartNode)
      newStartNode = newCh[++newStartIdx]
    }
  }

  if (oldStartIdx > oldEndIdx) {
    // 可能有新的 node 需要添加
    const refNode = newCh[newEndIdx + 1] || null
    insertNodes(parent, refNode, newCh, newStartIdx, newEndIdx)
  } else if (newStartIdx > newEndIdx) {
    // oldCh 中可能有多余的 node 需要删除
    removeNodes(parent, ch, oldStartIdx, oldEndIdx)
  }
}

function sameVnode(vnode, newVnode) {
  return vnode.tag === newVnode.tag && vnode.$$type === newVnode.$$type
}

function patchComponent(vnode, newVNode) {
  vnode.data = newVNode.data
  const { props, children } = vnode.componentOptions = newVNode.componentOptions
  if (vnode.instance) {
    vnode.instance.updateComponent(props, children)
  }
  return vnode
}

function patchVNode(vnode, newVNode) {
  if (isComponent(vnode)) return patchComponent(vnode, newVNode)

  const { el, data } = vnode
  const { style, attrs } = data
  const { style: newStyle, attrs: newAttrs } = newVNode.data

  getDifferent(style, newStyle).forEach(({ key, value }) => {
    el.style[key] = value
  })

  getDifferent(attrs, newAttrs).forEach(({ key, value }) => {
    el.setAttribute(key, value)
  })

  if (vnode.text !== undefined) {
    if (vnode.text !== newVNode.text) el.innerText = newVNode.text
  } else {
    diffChildren(vnode, newVNode)
  }

  vnode.data = newVNode.data
  vnode.text = newVNode.text

  return vnode
}

function getNodeSibling(ch, node) {
  const idx = ch.indexOf(node)
  return ch[idx + 1] || null
}

function insertNodes(parent, ref, newCh, start, end) {
  for (let i = start; i <= end; i++) {
    parent.insertBefore(newCh[i].createElement(parent), ref)
  }
}

function removeNodes(parent, oldCh, start, end) {
  for (let i = start; i <= end; i++) {
    parent.removeChild(oldCh[i])
  }
}

/**
 * getDifferent
 * @param obj
 * @param obj2
 * @returns {Array}
 */
function getDifferent(obj, obj2) {
  if (obj2 === obj) return []
  const keys = Object.keys(obj2)
  const res = []
  Object.keys(obj).forEach((key) => {
    remove(key, keys)
    if (obj2[key] === undefined || obj[key] !== obj2[key]) {
      res.push({ key, value: obj2[key] })
    }
  })
  if (keys.length > 0) {
    keys.forEach(key => res.push({ key, value: obj2[key] }))
  }
  return res
}
