/*
 * 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
 */
import Vm from './index';
import Element from '../../vdom/Element';
import Comment from '../../vdom/Comment';
import Node from '../../vdom/Node';
import { FragBlockInterface, isBlock } from './compiler';
import { emitSubVmLife } from './pageLife';

/**
 * Create a body by type.
 * @param {Vm} vm - Vm object.
 * @param {string} type - Element type.
 * @return {Node} Body of Node by type.
 */
export function createBody(vm: Vm, type: string): Node {
  const doc = vm._app.doc;
  return doc.createBody(type);
}

/**
 * Create an element by type
 * @param {Vm} vm - Vm object.
 * @param {string} type - Element type.
 * @return {Element} Element of Node by type.
 */
export function createElement(vm: Vm, type: string): Element {
  const doc = vm._app.doc;
  return doc.createElement(type);
}

/**
 * Create and return a frag block for an element.
 * @param {Vm} vm - Vm object.
 * @param {Element} element - Element object.
 * @return {FragBlockInterface} New block.
 */
export function createBlock(vm: Vm, element: Element | FragBlockInterface): FragBlockInterface {
  const start = createBlockStart(vm);
  const end = createBlockEnd(vm);
  const blockId = lastestBlockId++;
  const newBlock: FragBlockInterface = {start, end, blockId};
  if (isBlock(element)) {
    let updateMark = element.updateMark;
    if (updateMark) {
      if (isBlock(updateMark)) {
        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);
    }
    newBlock.element = element.element;
  } else {
    element.appendChild(start);
    element.appendChild(end);
    newBlock.element = element;
    element.block = newBlock;
  }
  return newBlock;
}

let lastestBlockId = 1;

/**
 * Create and return a block starter.
 * @param {Vm} vm - Vm object.
 * @return {Comment} A block starter.
 */
function createBlockStart(vm: Vm): Comment {
  const doc = vm._app.doc;
  const anchor = doc.createComment('start');
  return anchor;
}

/**
 * Create and return a block ender.
 * @param {Vm} vm - Vm object.
 * @return {Comment} A block starter.
 */
function createBlockEnd(vm: Vm): Comment {
  const doc = vm._app.doc;
  const anchor = doc.createComment('end');
  anchor.destroyHook = function() {
    if (anchor.watchers !== undefined) {
      anchor.watchers.forEach(function(watcher) {
        watcher.teardown();
      });
      anchor.watchers = [];
    }
  };
  return anchor;
}

/**
 * Attach target to a certain dest using appendChild by default.
 * @param {Element} target - If the dest is a frag block then insert before the ender.
 * @param {FragBlockInterface | Element} dest - A certain dest.
 * @return {*}
 */
export function attachTarget(target: Element, dest: FragBlockInterface | Element): any {
  if (isBlock(dest)) {
    const before = dest.end;
    const after = dest.updateMark;
    if (dest.children) {
      dest.children.push(target);
    }
    if (after) {
      const signal = moveTarget(target, after);
      if (isBlock(target)) {
        dest.updateMark = target.end;
      } else {
        dest.updateMark = target;
      }
      return signal;
    } else if (isBlock(target)) {
      dest.element.insertBefore(target.start, before);
      dest.element.insertBefore(target.end, before);
    } else {
      return dest.element.insertBefore(target, before);
    }
  } else {
    if (isBlock(target)) {
      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 {Element | FragBlockInterface} target - Block or element.
 * @param {Node} after - Node object after moving.
 * @return {*}
 */
export function moveTarget(target: Element | FragBlockInterface, after: Node): any {
  if (isBlock(target)) {
    return moveBlock(target, after);
  }
  return moveElement(target, after);
}

/**
 * Move element before a certain element.
 * @param {Element} element - Element object.
 * @param {Node} after - Node object after moving.
 * @return {*}
 */
function moveElement(element: Element, after: Node): any {
  const parent = after.parentNode as Element;
  if (parent && parent.children.indexOf(after) !== -1) {
    return parent.insertAfter(element, after);
  }
}

/**
 * Move all elements of the block before a certain element.
 * @param {FragBlockInterface} fragBlock - Frag block.
 * @param {Node} after - Node object after moving.
 */
function moveBlock(fragBlock: FragBlockInterface, after: Node): any {
  const parent = after.parentNode as Element;
  if (parent) {
    let el = fragBlock.start as Node;
    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.
 * @param {Element | FragBlockInterface} target - If the target is a frag block then call _removeBlock
 * @param {boolean} [preserveBlock] - Preserve block.
 */
export function removeTarget(target: Element | FragBlockInterface, preserveBlock?: boolean): void {
  if (!preserveBlock) {
    preserveBlock = false;
  }
  if (isBlock(target)) {
    removeBlock(target, preserveBlock);
  } else {
    removeElement(target);
  }
  if (target.vm) {
    target.vm.$emit('hook:onDetached');
    emitSubVmLife(target.vm, 'onDetached');
    target.vm.$emit('hook:destroyed');
  }
}

/**
 * Remove an element.
 * @param {Element | Comment} target - Target element.
 */
function removeElement(target: Element | Comment): void {
  const parent = target.parentNode as Element;
  if (parent) {
    parent.removeChild(target);
  }
}

/**
 * Remove a frag block.
 * @param {FragBlockInterface} fragBlock - Frag block.
 * @param {boolean} [preserveBlock] - If preserve block.
 */
function removeBlock(fragBlock: FragBlockInterface, preserveBlock?: boolean): void {
  if (!preserveBlock) {
    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);
  }
}