/*
 * Tencent is pleased to support the open source community by making
 * Hippy available.
 *
 * Copyright (C) 2017-2019 THL A29 Limited, a Tencent company.
 * All rights reserved.
 *
 * Licensed 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.
 */

/* eslint-disable no-underscore-dangle */
/* eslint-disable no-param-reassign */

import { insertChild, removeChild, moveChild } from '../renderer/render';
import { relativeToRefType } from '../utils/node';

let currentNodeId = 0;
function getNodeId() {
  currentNodeId += 1;
  // currentNodeId % 10 === 0 is rootView
  // It's a limitation of iOS SDK.
  if (currentNodeId % 10 === 0) {
    currentNodeId += 1;
  }
  return currentNodeId;
}

interface NodeMeta {
  skipAddToDom?: boolean;
  component: {
    name?: string;
    skipAddToDom?: boolean;
  };
}

class ViewNode {
  public nodeId: number;

  // Component meta information, such as native component will use.
  public meta: NodeMeta = {
    component: {},
  };

  // Index number in children, will update at traverseChildren method.
  public index = 0;

  // Relation nodes.
  public childNodes: ViewNode[] = [];

  public parentNode: ViewNode | null = null;

  // Will change to be true after insert into Native dom.
  private mounted = false;

  public constructor() {
    // Virtual DOM node id, will used in native to identify.
    this.nodeId = getNodeId();
  }

  /* istanbul ignore next */
  public toString() {
    return this.constructor.name;
  }

  public get isMounted() {
    return this.mounted;
  }

  public set isMounted(isMounted: boolean) {
    // TODO: Maybe need validation, maybe not.
    this.mounted = isMounted;
  }

  public insertBefore(childNode: ViewNode, referenceNode: ViewNode) {
    if (!childNode) {
      throw new Error('Can\'t insert child.');
    }
    if (childNode.meta.skipAddToDom) {
      return;
    }
    if (!referenceNode) {
      return this.appendChild(childNode);
    }
    if (referenceNode.parentNode !== this) {
      throw new Error('Can\'t insert child, because the reference node has a different parent.');
    }
    if (childNode.parentNode && childNode.parentNode !== this) {
      throw new Error('Can\'t insert child, because it already has a different parent.');
    }
    const index = this.childNodes.indexOf(referenceNode);
    childNode.parentNode = this;
    this.childNodes.splice(index, 0, childNode);
    return insertChild(
      this,
      childNode,
      { refId: referenceNode.nodeId, relativeToRef: relativeToRefType.BEFORE },
    );
  }

  public moveChild(childNode: ViewNode, referenceNode: ViewNode) {
    if (!childNode) {
      throw new Error('Can\'t move child.');
    }
    if (childNode.meta.skipAddToDom) {
      return;
    }
    if (!referenceNode) {
      return this.appendChild(childNode);
    }
    if (referenceNode.parentNode !== this) {
      throw new Error('Can\'t move child, because the reference node has a different parent.');
    }
    if (childNode.parentNode && childNode.parentNode !== this) {
      throw new Error('Can\'t move child, because it already has a different parent.');
    }
    const oldIndex = this.childNodes.indexOf(childNode);
    const referenceIndex = this.childNodes.indexOf(referenceNode);
    // return if the moved index is the same as the previous one
    if (referenceIndex === oldIndex) {
      return childNode;
    }
    this.childNodes.splice(oldIndex, 1);
    const newIndex = this.childNodes.indexOf(referenceNode);
    this.childNodes.splice(newIndex, 0, childNode);
    return moveChild(
      this,
      childNode,
      { refId: referenceNode.nodeId, relativeToRef: relativeToRefType.BEFORE },
    );
  }

  public appendChild(childNode: ViewNode) {
    if (!childNode) {
      throw new Error('Can\'t append child.');
    }
    if (childNode.meta.skipAddToDom) {
      return;
    }
    if (childNode.parentNode && childNode.parentNode !== this) {
      throw new Error('Can\'t append child, because it already has a different parent.');
    }
    childNode.parentNode = this;
    const referenceIndex = this.childNodes.length - 1;
    const referenceNode = this.childNodes[referenceIndex];
    this.childNodes.push(childNode);
    insertChild(
      this,
      childNode,
      referenceNode && { refId: referenceNode.nodeId, relativeToRef: relativeToRefType.AFTER },
    );
  }

  public removeChild(childNode: ViewNode) {
    if (!childNode) {
      throw new Error('Can\'t remove child.');
    }
    if (childNode.meta.skipAddToDom) {
      return;
    }
    if (!childNode.parentNode) {
      throw new Error('Can\'t remove child, because it has no parent.');
    }
    if (childNode.parentNode !== this) {
      throw new Error('Can\'t remove child, because it has a different parent.');
    }
    const index = this.childNodes.indexOf(childNode);
    this.childNodes.splice(index, 1);
    removeChild(this, childNode);
  }

  /**
   * Find a specific target with condition
   */
  public findChild(condition: Function): ViewNode | null {
    const yes = condition(this);
    if (yes) {
      return this;
    }
    if (this.childNodes.length) {
      for (let i = 0; i < this.childNodes.length; i += 1) {
        const childNode = this.childNodes[i];
        const targetChild = this.findChild.call(childNode, condition);
        if (targetChild) {
          return targetChild;
        }
      }
    }
    return null;
  }

  /**
   * Traverse the children and execute callback
   * @param callback - callback function
   * @param refInfo - reference node info
   */
  public traverseChildren(callback: Function, refInfo) {
    callback(this, refInfo);
    // Find the children
    if (this.childNodes.length) {
      this.childNodes.forEach((childNode) => {
        this.traverseChildren.call(childNode, callback, {});
      });
    }
  }
}

export default ViewNode;
