import { camelCase, snakeCase, kebabCase } from 'skypager-util/lib/string'
import cloneDeep from 'lodash/cloneDeep'

export default (doc) => {
  const ast = doc.ast

  if (!doc.ast) {
    console.log('Could not find', doc)
  }

  doc.ast = Object.assign(ast, {
    children: ast.children.map(node => decorateNode(doc, node))
  })

  if (!doc.chain) {
    Object.defineProperty(doc, 'chain', {
      enumerable: false,
      get: function() {
        return __non_webpack_require__('lodash').chain(doc)
      }
    })
  }

  return Object.assign(doc, {
    decorateNode(node) {
      return decorateNode(doc, node)
    },

    createTree() {
      const headings = doc.createHeadingsMap('headingPath')

      const tree = {}

      Object.keys(headings).forEach(headingPath => {
        const treePath = headingPath.split('/').map(v => camelCase(kebabCase(v)))
        lodash.set(tree, treePath, headings[headingPath])
      })

      return tree
    },

    createHeadingsMap(keyBy = 'accessor', stayChained = false) {
      const obj = doc.chain
        .invoke('selectHeadingNodes')
        .map(heading => {
          heading.accessorMethod = camelCase(snakeCase(heading.textContent()))
          heading.headingPath = heading.getHeadingPath()
          heading.accessors = heading.headingPath.split('/').map(i => camelCase(snakeCase(i)))
          heading.accessor = camelCase(snakeCase(heading.headingPath.replace(/\//g,'_')))
          return heading
        })
        .keyBy(keyBy)

      return stayChained ? obj : obj.value()
    },

    stringifyNode(node) {
      return require('mdast-util-to-string')(node)
    },

    getSourceForNode(node) {
      return require('unist-util-source')(node)
    },

    headingRange(test) {
      const ast = doc.ast

      return new Promise((resolve,reject) => {
        try {
          require('mdast-util-heading-range')(ast, test, (startNode, nodes, endNode) => {
            resolve({startNode, nodes, endNode})
          })
        } catch(error) {
          reject(error)
        }
      })
    },

    documentTitle() {
      const titleNode = doc.titleNode()

      if (!titleNode) {
        return doc.get('frontmatter.title', doc.variations.humanized)
      } else {
        return titleNode.textContent()
      }
    },

    introParagraphs() {
      const until = doc.selectHeadingNodes()[1] || doc.finalNode()

      if (!until) {
        return doc.selectNodes('paragraph')
      } else {
        const children = until.getNodesBefore((node) => (
          ['paragraph'].indexOf(node.type) >= 0
        )).reverse()

        return require('mdast-util-to-string')({type:'root', children})
      }
    },

    withoutNodeIndexes(indexList = []) {
      const ast = this.ast

      return {
        type: 'root',
        children: children.filter(node => indexList.indexOf(node.index) === -1),
        data: ast.data,
      }
    },

    createAST(options = {}) {
      const newAST = {
        type: options.type || 'root',
        children: options.children || [],
        data: options.data || {},
      }

      return newAST
    },

    extractSectionNodes(node) {
      return node.headingRange().then(({nodes}) => nodes)
    },

    inspectOutline() {
      const headings = this.selectHeadingNodes()

      return headings.map(headingNode => {
        const content = `- ${headingNode.textContent()}`
        return lodash.padStart(content, content.length + (headingNode.depth - 1) * 2)
      }).join("\n")
    },

    headingIndexes(depth) {
      return this.chain
        .invoke('selectHeadingNodes', depth && `[depth=${depth}]`)
        .sortBy(node => node.position ? node.position.start.line : node.index)
        .map(node => node.index)
        .value()
    },

    references() {
      return Object.assign({}, this.referenceImages(), this.referenceLinks())
    },

    referenceImages() {
      return this.chain
        .invoke('selectNodes', 'imageReference')
        .keyBy('identifier')
        .mapValues((refLink, identifier) => {
          const definitionNodes = doc.definitionNodes()

          return Object.assign(refLink, {
            definitionNode() {
              return definitionNodes[identifier]
            },
            get definition() {
              return lodash.pick(definitionNodes[identifier], 'title', 'url', 'identifier')
            }
          })
        })
        .value()
    },

    imageNodes() {
      return this.selectNodes('image')
    },

    referenceLinks() {
      return this.chain
        .invoke('selectNodes', 'linkReference')
        .keyBy('identifier')
        .mapValues((refLink, identifier) => {
          const definitionNodes = doc.definitionNodes()

          return Object.assign(refLink, {
            definitionNode() {
              return definitionNodes[identifier]
            },
            get definition() {
              return lodash.pick(definitionNodes[identifier], 'title', 'url', 'identifier')
            }
          })
        })
        .value()
    },

    definitionNodes() {
      return this.chain
        .invoke('selectNodes', 'definition')
        .keyBy('identifier')
        .value()
    },

    selectHeadingsByDepth(depth = 1) {
      return this.selectHeadingNodes(`[depth=${depth}]`)
    },

    selectOneNode(selector) {
      return require('unist-util-select')(this.ast)(selector)[0]
    },

    selectNodes(selector = '*', testFn) {
      const results = require('unist-util-select')(this.ast)(selector)

      return typeof testFn === 'function'
        ? results.filter(r => testFn.call(doc, r))
        : results
    },

    selectHeadingNodes(enhance = '') {
      return doc.selectNodes(`heading${enhance}`.trim())
    },

    selectCodeBlocks(lang = 'javascript', enhance='') {
      return doc.selectNodes(`code[lang=${lang}]${enhance}`.trim())
    },

    findNodeAfter(node, ...args) {
      return require('unist-util-find-after')(this.ast, node, ...args)
    },

    findAllNodesAfter(node, ...args) {
      return require('unist-util-find-all-after')(this.ast, node, ...args)
    },

    findAllNodesBefore(node, ...args) {
      return require('unist-util-find-all-before')(this.ast, node, ...args)
    },

    nodesIndexedByPosition() {
      return doc.chain
        .get('ast.children')
        .keyBy(node => parseInt(node.position.start.line))
        .value()
    },

    introNodes() {
      const f = this.selectNodes('heading')
      const m = f.find(n => n.depth > 1)

      const nodes = m
        ? m.getNodesBefore().reverse()
        : this.ast.children

      return nodes.filter(n => n.type !== 'yaml')
    },

    finalHeading() {
      return this.selectNodes('heading:last-of-type')
    },

    titleNode() {
      return this.selectOneNode('heading[depth=1]') || this.selectOneNode('heading:first-of-type')
    },

    firstSectionHeadingNode() {
      return this.selectOneNode('heading[depth=2]')
    },

    sectionHeadingNodes() {
      return this.selectNodes('heading[depth=2]')
    },

    articleHeadingNodes() {
      return this.selectNodes('heading[depth=3]')
    },

    subarticleHeadingNodes() {
      return this.selectNodes('heading').filter(n => n.depth >= 4)
    },

    getNodesBetweenPositions(alpha, bravo) {
      const start = isNode(alpha) ? alpha.position.start.line : parseInt(alpha)
      const end = isNode(bravo) ? bravo.position.start.line : parseInt(bravo)

      return doc.chain.get('ast.children').filter(node => {
        const { position: {
          start: {
            line
          }
        }} = node

        return line > start && line < end
      })
    },

    firstNode() {
      return this.chain.get('ast.children', []).first().value()
    },

    finalNode() {
      return this.chain.get('ast.children', []).last().value()
    },
  })
}


export function decorateNode(doc, node) {
    const original = cloneDeep(node)

    return Object.assign(node, {
      original() {
        return original
      },

      slug() {
        return kebabCase(this.textContent())
      },

      textContent() {
        return require('mdast-util-to-string')(node) || ''
      },

      getHeadingPath() {
        if (node.type !== 'heading') {
          const prev = node.previousHeading()
          return prev && prev.getHeadingPath()
        }

        const textContent = kebabCase(node.textContent().toLowerCase())
        const prevSuperior = node.getPreviousSuperior()
        const previousPath = typeof prevSuperior === 'undefined'
          ? ``
          : (prevSuperior.depth >= 2 && prevSuperior.getHeadingPath()) || ''

        return [previousPath, textContent].filter(i => i && i.length > 0).join('/')
      },

      headingRangeSync() {
        const alpha = node
        const bravo = node.getFinalHeading() || doc.finalNode()

        return doc.getNodesBetweenPositions(alpha, bravo || 50000).value()
      },

      headingRange(test = node.textContent()) {
        const ast = doc.ast

        return new Promise((resolve,reject) => {
          try {
            require('mdast-util-heading-range')(ast, test, (startNode, nodes, endNode) => {
              resolve({startNode, nodes, endNode})
            })
          } catch(error) {
            reject(error)
          }
        })
      },

      getFinalHeading() {
        return doc.findAllNodesAfter(node, (n) => n.type === 'heading' && n.depth <= node.depth )[0]
      },

      getPreviousSuperior() {
        return node && node.getNodesBefore && node.getNodesBefore(n => n.type === 'heading' && (n.depth < node.depth))[0]
      },

      previousHeading() {
        return node.getNodesBefore(n => n.type === 'heading').shift()
      },

      nextHeading() {
        return node.type === 'heading'
          ? node.getNextNode((n) => n.type === 'heading' && n.depth >= node.depth)
          : node.getNextNode(n => n.type === 'heading')
      },

      getNextNode(...args) {
        return doc.findNodeAfter(node, ...args)
      },

      getNodesAfter(...args) {
        return doc.findAllNodesAfter(node, ...args)
      },

      getNodesBefore(...args) {
        return doc.findAllNodesBefore(node, ...args)
      },

      selectChildren(selector, testFn) {
        if(node.type === 'heading') {
          return node.selectHeadingChildren(selector, testFn)
        }

        const results = require('unist-util-select')(node)(selector)

        return typeof testFn === 'function'
          ? results.filter(r => testFn.call(doc, r))
          : results
      },

      virtualAST() {
        return {
          type: 'root',
          children: node.headingRangeSync().map(n => n.original()).map((n,i) => {
            n.index = i
            return n
          })
        }
      },

      selectHeadingChildrenSync(selector = '*', testFn) {
        let results = require('unist-util-select')(node.virtualAST())(selector)

        results = typeof testFn === 'function'
          ? results.filter(r => testFn.call(doc, r))
          : results

        return results
      },

      selectHeadingChildren(selector = '*', testFn) {
        return node.headingRange()
          .then(({nodes}) => {
            const base = {
              type: 'root',
              children: nodes
            }

            return require('unist-util-select')(base)(selector)
          })
          .then((results) => {
            return typeof testFn === 'function'
              ? results.filter(r => testFn.call(doc, r))
              : results
          })
      }
    })
}

export function isNode(obj) {
  return typeof obj === 'object' && typeof obj.type === 'string' && obj.position
}

export function createBlock(contents, lang='') {
  return ["```" + lang, contents, "```"].join("\n")
}
