import Node from '../lib/Node'
import Selection from '../lib/Selection'

import find from '../utils/find'
import objectToNode from '../utils/objectToNode'
import { List } from '../utils/stack'
import { TreeParser } from '../utils/treeParser'
import { recurseDown } from '../utils/recurse'
import { get, createTemplate } from '../utils/request'
import sort from '../utils/sort'
import fetchDelay from '../utils/fetchDelay'

export default class Tree {
  constructor (vm) {
    this.vm = vm
    this.options = vm.opts

    this.activeElement = null

    // We have to convert 'fetchData' to function. It must return Promise always
    const fetchData = this.options.fetchData

    if (typeof fetchData === 'string') {
      this.options.fetchData = ((template) => {
        const urlTemplate = createTemplate(template)

        return node => {
          return get(urlTemplate(node)).catch(this.options.onFetchError)
        }
      })(fetchData)
    }
  }

  $on (name, ...args) {
    this.vm.$on(name, ...args)
  }

  $once (name, ...args) {
    this.vm.$once(name, ...args)
  }

  $off (name, ...args) {
    this.vm.$off(name, ...args)
  }

  $emit (name, ...args) {
    if (this.__silence) {
      return
    }

    this.vm.$emit(name, ...args)

    if (this.options.store) {
      this.vm.$emit('LIQUOR_NOISE')
    }
  }

  _sort (source, compareFn, deep) {
    if (deep !== false) {
      this.recurseDown(source, node => {
        if (node.hasChildren()) {
          sort(node.children, compareFn)
        }
      })
    }

    sort(source, compareFn)
  }

  sortTree (compareFn, deep) {
    this._sort(this.model, compareFn, deep)
  }

  sort (query, compareFn, deep) {
    const targetNode = this.find(query, true)

    if (!targetNode || !compareFn) {
      return
    }

    targetNode.forEach(node => {
      this._sort(node.children, compareFn, deep)
    })
  }

  clearFilter () {
    this.recurseDown(node => {
      node.state('matched', false)
      node.state('visible', true)
      node.state('expanded', node.__expanded)

      node.__expanded = undefined
      node.showChildren = true
    })

    this.vm.matches.length = 0
    this.vm.$emit('tree:filtered', [], '')
  }

  filter (query) {
    if (!query) {
      return this.clearFilter()
    }

    const matches = []
    const predicate = this.options.filter.matcher
    const { showChildren, plainList } = this.options.filter

    // collect nodes
    this.recurseDown(node => {
      if (predicate(query, node)) {
        matches.push(node)
      }

      node.showChildren = true

      // save prev `expanded` state
      if (undefined === node.__expanded) {
        node.__expanded = node.state('expanded')
      }

      node.state('visible', false)
      node.state('matched', false)
      node.state('expanded', true)
    })

    matches.reverse().forEach(node => {
      node.state('matched', true)
      node.state('visible', true)

      node.showChildren = !plainList

      if (node.hasChildren()) {
        node.recurseDown(n => {
          n.state('visible', !!showChildren)
        }, true)
      }

      node.recurseUp(parent => {
        parent.state('visible', true)
        parent.state('expanded', true)
      })

      if (node.hasChildren()) {
        node.state('expanded', false)
      }
    })

    this.vm.matches = matches

    this.vm.$emit('tree:filtered', matches, query)

    return matches
  }

  selected () {
    return new Selection(this, ...this.selectedNodes)
  }

  checked () {
    if (!this.options.checkbox) {
      return null
    }

    return new Selection(this, ...this.checkedNodes)
  }

  loadChildren (node) {
    if (!node) {
      return
    }

    this.$emit('tree:data:fetch', node)

    if (this.options.minFetchDelay > 0 && node.vm) {
      node.vm.loading = true
    }

    const result = this.fetch(node)
      .then(children => {
        node.append(children)
        node.isBatch = false

        if (this.options.autoCheckChildren) {
          if (node.checked()) {
            node.recurseDown(child => {
              child.state('checked', true)
            })
          }

          node.refreshIndeterminateState()
        }

        this.$emit('tree:data:received', node)
      })

    return Promise.all([
      fetchDelay(this.options.minFetchDelay),
      result
    ]).then(_ => {
      if (node.vm) {
        node.vm.loading = false
      }

      return result
    })
  }

  fetch (node, parseData) {
    let result = this.options.fetchData(node)

    if (!result.then) {
      result = get(result)
        .catch(this.options.onFetchError)
    }

    if (parseData === false) {
      return result
    }

    return result
      .then(data => {
        try {
          return this.parse(data, this.options.modelParse)
        } catch (e) {
          throw new Error(e)
        }
      })
      .catch(this.options.onFetchError)
  }

  fetchInitData () {
    // simulate root node
    const node = {
      id: 'root',
      name: 'root'
    }

    return this.fetch(node, false)
  }

  setModel (data) {
    return new Promise(resolve => {
      this.model = this.parse(data, this.options.modelParse)

      /* eslint-disable */
      requestAnimationFrame(_ => {
        this.vm.model = this.model
        resolve()
      })
      /* eslint-enable */

      /**
      * VueJS transform properties to reactives when constructor is running
      * And we lose List object (extended from Array)
      */
      this.selectedNodes = new List()
      this.checkedNodes = new List()

      recurseDown(this.model, node => {
        node.tree = this

        if (node.selected()) {
          this.selectedNodes.add(node)
        }

        if (node.checked()) {
          this.checkedNodes.add(node)

          if (node.parent) {
            node.parent.refreshIndeterminateState()
          }
        }

        if (this.options.autoDisableChildren && node.disabled()) {
          node.recurseDown(child => {
            child.state('disabled', true)
          })
        }
      })

      if (!this.options.multiple && this.selectedNodes.length) {
        const top = this.selectedNodes.top()

        this.selectedNodes.forEach(node => {
          if (top !== node) {
            node.state('selected', false)
          }
        })

        this.selectedNodes
          .empty()
          .add(top)
      }

      // Nodes can't be selected on init. By it's possible to select through API
      if (this.options.checkOnSelect && this.options.checkbox) {
        this.unselectAll()
      }
    })
  }

  recurseDown (node, fn) {
    if (!fn && node) {
      fn = node
      node = this.model
    }

    if (typeof fn !== 'function') {
      new TypeError('Argument must be a function')
    }

    return recurseDown(node, fn)
  }

  select (node, extendList) {
    const treeNode = this.getNode(node)

    if (!treeNode) {
      return false
    }

    if (this.options.multiple && extendList) {
      this.selectedNodes.add(treeNode)
    } else {
      this.unselectAll()
      this.selectedNodes
        .empty()
        .add(treeNode)
    }

    return true
  }

  selectAll () {
    if (!this.options.multiple) {
      return false
    }

    this.selectedNodes.empty()

    this.recurseDown(node => {
      this.selectedNodes.add(
        node.select(true)
      )
    })

    return true
  }

  unselect (node) {
    const treeNode = this.getNode(node)

    if (!treeNode) {
      return false
    }

    this.selectedNodes.remove(treeNode)

    return true
  }

  unselectAll () {
    let node

    while (node = this.selectedNodes.pop()) {
      node.unselect()
    }

    return true
  }

  check (node) {
    this.checkedNodes.add(node)
  }

  uncheck (node) {
    this.checkedNodes.remove(node)
  }

  checkAll () {
    this.recurseDown(node => {
      if (node.depth === 0) {
        if (node.indeterminate()) {
          node.state('indeterminate', false)
        }

        node.check()
      }
    })
  }

  uncheckAll () {
    let node

    while (node = this.checkedNodes.pop()) {
      node.uncheck()
    }

    return true
  }

  expand (node) {
    if (node.expanded()) {
      return false
    }

    node.expand()

    return true
  }

  collapse (node) {
    if (node.collapsed()) {
      return false
    }

    node.collapse()

    return true
  }

  toggleExpand (node) {
    if (!node.hasChildren()) {
      return false
    }

    node.toggleExpand()

    return true
  }

  toggleCollapse (node) {
    if (!node.hasChildren()) {
      return false
    }

    node.toggleCollapse()

    return true
  }

  expandAll () {
    this.recurseDown(node => {
      if (node.hasChildren() && node.collapsed()) {
        node.expand()
      }
    })
  }

  collapseAll () {
    this.recurseDown(node => {
      if (node.hasChildren() && node.expanded()) {
        node.collapse()
      }
    })
  }

  index (node, verbose) {
    let target = node.parent

    if (target) {
      target = target.children
    } else {
      target = this.model
    }

    const index = target.indexOf(node)

    if (verbose) {
      return {
        index: index,
        target,
        node: target[index]
      }
    }

    return index
  }

  nextNode (node) {
    const { target, index } = this.index(node, true)

    return target[index + 1] || null
  }

  nextVisibleNode (node) {
    if (node.hasChildren() && node.expanded()) {
      return node.first()
    }

    const nextNode = this.nextNode(node)

    if (!nextNode && node.parent) {
      return node.parent.next()
    }

    return nextNode
  }

  prevNode (node) {
    const { target, index } = this.index(node, true)

    return target[index - 1] || null
  }

  prevVisibleNode (node) {
    const prevNode = this.prevNode(node)

    if (!prevNode) {
      return node.parent
    }

    if (prevNode.hasChildren() && prevNode.expanded()) {
      return prevNode.last()
    }

    return prevNode
  }

  addToModel (node, index = this.model.length) {
    node = this.objectToNode(node)

    this.model.splice(index, 0, node)
    this.recurseDown(node, n => {
      n.tree = this
    })

    this.$emit('node:added', node)

    return node
  }

  append (criteria, node) {
    const targetNode = this.find(criteria)

    if (targetNode) {
      return targetNode.append(node)
    }

    return false
  }

  prepend (criteria, node) {
    const targetNode = this.find(criteria)

    if (targetNode) {
      return targetNode.prepend(node)
    }

    return false
  }

  before (targetNode, sourceNode) {
    targetNode = this.find(targetNode)

    const position = this.index(targetNode, true)
    const node = this.objectToNode(sourceNode)

    if (!~position.index) {
      return false
    }

    position.target.splice(
      position.index,
      0,
      node
    )

    node.parent = targetNode.parent
    this.$emit('node:added', node)

    return node
  }

  after (targetNode, sourceNode) {
    targetNode = this.find(targetNode)

    const position = this.index(targetNode, true)
    const node = this.objectToNode(sourceNode)

    if (!~position.index) {
      return false
    }

    position.target.splice(
      position.index + 1,
      0,
      node
    )

    node.parent = targetNode.parent
    this.$emit('node:added', node)

    return node
  }

  addNode (node) {
    const index = this.model.length

    node = objectToNode(node)

    this.model.splice(index, 0, node)
    this.$emit('node:added', node)

    return node
  }

  remove (criteria, multiple) {
    return this.removeNode(
      this.find(criteria, multiple)
    )
  }

  removeNode (node) {
    if (node instanceof Selection) {
      return node.remove()
    }

    if (!node) {
      return false
    }

    if (!node.parent) {
      if (~this.model.indexOf(node)) {
        this.model.splice(
          this.model.indexOf(node),
          1
        )
      }
    } else {
      const children = node.parent.children

      if (~children.indexOf(node)) {
        children.splice(
          children.indexOf(node),
          1
        )
      }
    }

    if (node.parent) {
      if (node.parent.indeterminate() && !node.parent.hasChildren()) {
        node.parent.state('indeterminate', false)
      }
    }

    if (this.activeElement !== null) {
      if (node.id === this.activeElement.id) {
        this.activeElement = null
      }
    }

    node.parent = null

    this.$emit('node:removed', node)

    this.selectedNodes.remove(node)
    this.checkedNodes.remove(node)

    const matches = this.vm.matches

    if (matches && matches.length) {
      if (matches.includes(node)) {
        matches.splice(
          matches.indexOf(node),
          1
        )
      }
    }

    return node
  }

  isNode (node) {
    return node instanceof Node
  }

  find (criteria, multiple) {
    if (this.isNode(criteria)) {
      return criteria
    }

    const result = find(this.model, criteria)

    if (!result || !result.length) {
      return new Selection(this, [])
    }

    if (multiple === true) {
      return new Selection(this, result)
    }

    return new Selection(this, [result[0]])
  }

  updateData (criteria, callback) {
    const nodes = this.find(criteria)

    nodes.forEach(node => node.setData(callback(node)))

    return nodes
  }

  getNodeById (id) {
    let targetNode = null

    recurseDown(this.model, node => {
      if ('' + node.id === id) {
        targetNode = node
        return false
      }
    })

    return targetNode
  }

  getNode (node) {
    if (this.isNode(node)) {
      return node
    }

    return null
  }

  objectToNode (obj) {
    return objectToNode(this, obj)
  }

  parse (data, options) {
    if (!options) {
      options = this.options.propertyNames
    }

    try {
      return TreeParser.parse(data, this, options)
    } catch (e) {
      return []
    }
  }
}
