/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
/**
 * @fileOverview Document & Element Helpers.
 *
 * required:
 * Document#: createElement, createComment, getRef
 * Element#: appendChild, insertBefore, removeChild, nextSibling
 */

/**
 * Create a body by type
 * Using this._app.doc
 *
 * @param  {string} type
 */
export function createBody (vm, type) {
  const doc = vm._app.doc
  return doc.createBody(type)
}

/**
 * Create an element by type
 * Using this._app.doc
 *
 * @param  {string} type
 */
export function createElement (vm, type) {
  const doc = vm._app.doc
  return doc.createElement(type)
}

/**
 * Create and return a frag block for an element.
 * The frag block has a starter, ender and the element itself.
 *
 * @param  {object} element
 */
export function createBlock (vm, element) {
  const start = createBlockStart(vm)
  const end = createBlockEnd(vm)
  const blockId = lastestBlockId++
  if (element.element) {
    let updateMark = element.updateMark
    if (updateMark) {
      if (updateMark.element) {
        updateMark = updateMark.end
      }
      element.element.insertAfter(end, updateMark)
      element.element.insertAfter(start, updateMark)
      element.updateMark = end
    }
    else {
      element.element.insertBefore(start, element.end)
      element.element.insertBefore(end, element.end)
    }
    element = element.element
  }
  else {
    element.appendChild(start)
    element.appendChild(end)
  }
  return { start, end, element, blockId }
}

let lastestBlockId = 1

/**
 * Create and return a block starter.
 * Using this._app.doc
 */
function createBlockStart (vm) {
  const doc = vm._app.doc
  const anchor = doc.createComment('start')
  return anchor
}

/**
 * Create and return a block ender.
 * Using this._app.doc
 */
function createBlockEnd (vm) {
  const doc = vm._app.doc
  const anchor = doc.createComment('end')
  return anchor
}

/**
 * Attach target to a certain dest using appendChild by default.
 * If the dest is a frag block then insert before the ender.
 * If the target is a frag block then attach the starter and ender in order.
 *
 * @param  {object} target
 * @param  {object} dest
 */
export function attachTarget (vm, target, dest) {
  if (dest.element) {
    const before = dest.end
    const after = dest.updateMark
    // push new target for watch list update later
    if (dest.children) {
      dest.children.push(target)
    }
    // for check repeat case
    if (after) {
      const signal = moveTarget(vm, target, after)
      dest.updateMark = target.element ? target.end : target
      return signal
    }
    else if (target.element) {
      dest.element.insertBefore(target.start, before)
      dest.element.insertBefore(target.end, before)
    }
    else {
      return dest.element.insertBefore(target, before)
    }
  }
  else {
    if (target.element) {
      dest.appendChild(target.start)
      dest.appendChild(target.end)
    }
    else {
      return dest.appendChild(target)
    }
  }
}

/**
 * Move target before a certain element. The target maybe block or element.
 *
 * @param  {object} target
 * @param  {object} before
 */
export function moveTarget (vm, target, after) {
  if (target.element) {
    return moveBlock(target, after)
  }
  return moveElement(target, after)
}

/**
 * Move element before a certain element.
 *
 * @param  {object} element
 * @param  {object} before
 */
function moveElement (element, after) {
  const parent = after.parentNode
  if (parent) {
    return parent.insertAfter(element, after)
  }
}

/**
 * Move all elements of the block before a certain element.
 *
 * @param  {object} fragBlock
 * @param  {object} before
 */
function moveBlock (fragBlock, after) {
  const parent = after.parentNode

  if (parent) {
    let el = fragBlock.start
    let signal
    const group = [el]

    while (el && el !== fragBlock.end) {
      el = el.nextSibling
      group.push(el)
    }

    let temp = after
    group.every((el) => {
      signal = parent.insertAfter(el, temp)
      temp = el
      return signal !== -1
    })

    return signal
  }
}

/**
 * Remove target from DOM tree.
 * If the target is a frag block then call _removeBlock
 *
 * @param  {object} target
 */
export function removeTarget (vm, target, preserveBlock = false) {
  if (target.element) {
    removeBlock(target, preserveBlock)
  }
  else {
    removeElement(target)
  }
  if (target._vm) {
    target._vm.$emit('hook:destroyed')
  }
}

/**
 * Remove a certain element.
 * Using this._app.doc
 *
 * @param  {object} target
 */
function removeElement (target) {
  const parent = target.parentNode

  if (parent) {
    parent.removeChild(target)
  }
}

/**
 * Remove a frag block.
 * The second param decides whether the block self should be removed too.
 *
 * @param  {object}  fragBlock
 * @param  {Boolean} preserveBlock=false
 */
function removeBlock (fragBlock, preserveBlock = false) {
  const result = []
  let el = fragBlock.start.nextSibling

  while (el && el !== fragBlock.end) {
    result.push(el)
    el = el.nextSibling
  }

  if (!preserveBlock) {
    removeElement(fragBlock.start)
  }
  result.forEach((el) => {
    removeElement(el)
  })
  if (!preserveBlock) {
    removeElement(fragBlock.end)
  }
}
