/*************************************************************
 *
 *  Copyright (c) 2017 The MathJax Consortium
 *
 *  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.
 */

/**
 * @fileoverview Generic Node classes for node trees
 *
 * @author dpvc@mathjax.org (Davide Cervone)
 */

import {NodeFactory} from './NodeFactory.js';

/**
 *  PropertyList and Property are for string data like
 *  attributes and other properties
 */
export type Property = string | number | boolean;
export type PropertyList = {[key: string]: Property};

/*********************************************************/
/**
 *  The generic Node interface
 */

export interface Node {
  readonly kind: string;
  parent: Node;
  childNodes: Node[];

  /**
   * @param {string} name     The name of the property to set
   * @param {Property} value  The value to which the property will be set
   */
  setProperty(name: string, value: Property): void;

  /**
   * @param {string} name  The name of the property to get
   * @return {Property}   The value of the named property
   */
  getProperty(name: string): Property;

  /**
   * @return {string[]}  An array of the names of every property currently defined
   */
  getPropertyNames(): string[];

  /**
   * @return {PropertyList}  The propery list containing all the properties of the node
   */
  getAllProperties(): PropertyList;

  /**
   * @param {string[]} names  The names of the properties to be removed
   */
  removeProperty(...names: string[]): void;


  /**
   * @param {string} kind  The type of node to test for
   * @return {boolean}     True when the node is of the given type
   */
  isKind(kind: string): boolean;

  /**
   * @param {Node[]} children  The child nodes to add to this node
   */
  setChildren(children: Node[]): void;

  /**
   * @param {Node} child  A node to add to this node's children
   * @return {Node}       The child node that was added
   */
  appendChild(child: Node): Node;

  /**
   * @param {Node} newChild  A child node to be inserted
   * @param {Node} oldChild  A child node to be replaced
   * @return {Node}          The old child node that was removed
   */
  replaceChild(newChild: Node, oldChild: Node): Node;

  /**
   * @param {Node} child  A child node whose index in childNodes is desired
   * @return {number}     The index of the child in childNodes, or null if not found
   */
  childIndex(child: Node): number;

  /**
   * @param {string} kind  The kind of nodes to be located in the tree
   * @return {Node[]}      An array of nodes that are children (at any depth) of the given kind
   */
  findNodes(kind: string): Node[];

  /**
   * @param {Function} func  A function to apply to each node in the tree rooted at this node
   * @param {any} data       Data to pass to the function (as state information)
   */
  walkTree(func: (node: Node, data?: any) => void, data?: any): void;
}

/*********************************************************/
/**
 *  The generic Node class interface
 */

export interface NodeClass {
  /**
   * @param {NodeFactory} factory  The NodeFactory to use to create new nodes when needed
   * @param {PropertyList} properties  Any properties to be added to the node, if any
   * @param {Node[]} children  The initial child nodes, if any
   * @return {Node}  The newly created node
   */
  new (factory: NodeFactory<Node, NodeClass>, properties?: PropertyList, children?: Node[]): Node;
}

/*********************************************************/
/**
 *  The abstract Node class
 */

export abstract class AbstractNode implements Node {

  /**
   * The parent node for this one
   */
  public parent: Node = null;

  /**
   * The properties for this node
   */
  protected properties: PropertyList = {};

  /**
   * The NodeFactory to use to create additional nodes, as needed
   */
  protected _factory: NodeFactory<Node, NodeClass> = null;

  /**
   * The children for this node
   */
  public childNodes: Node[] = [];

  /**
   * @param {NodeFactory} factory  The NodeFactory to use to create new nodes when needed
   * @param {PropertyList} properties  Any properties to be added to the node, if any
   * @param {Node[]} children  The initial child nodes, if any
   *
   * @constructor
   * @implements {Node}
   */
  constructor(factory: NodeFactory<Node, NodeClass>, properties: PropertyList = {}, children: Node[] = []) {
    this._factory = factory;
    for (const name of Object.keys(properties)) {
      this.setProperty(name, properties[name]);
    }
    if (children.length) {
      this.setChildren(children);
    }
  }

  /**
   * @override
   */
  public get factory () {
    return this._factory;
  }

  /**
   * @override
   */
  public get kind() {
    return 'unknown';
  }

  /**
   * @override
   */
  public setProperty(name: string, value: Property) {
    this.properties[name] = value;
  }

  /**
   * @override
   */
  public getProperty(name: string) {
    return this.properties[name];
  }

  /**
   * @override
   */
  public getPropertyNames() {
    return Object.keys(this.properties);
  }

  /**
   * @override
   */
  public getAllProperties() {
    return this.properties;
  }

  /**
   * @override
   */
  public removeProperty(...names: string[]) {
    for (const name of names) {
      delete this.properties[name];
    }
  }


  /**
   * @override
   */
  public isKind(kind: string): boolean {
    return this.factory.nodeIsKind(this, kind);
  }


  /**
   * @override
   */
  public setChildren(children: Node[]) {
    this.childNodes = [];
    for (let child of children) {
      this.appendChild(child);
    }
  }

  /**
   * @override
   */
  public appendChild(child: Node) {
    this.childNodes.push(child);
    child.parent = this;
    return child;
  }

  /**
   * @override
   */
  public replaceChild(newChild: Node, oldChild: Node) {
    let i = this.childIndex(oldChild);
    // If i === null should we error?  return null?  silently fail?
    if (i !== null) {
      this.childNodes[i] = newChild;
      newChild.parent = this;
    }
    return newChild;
  }


  /**
   * @override
   */
  public childIndex(node: Node) {
    let i = this.childNodes.indexOf(node);
    return (i === -1 ? null : i);
  }


  /**
   * @override
   */
  public findNodes(kind: string) {
    let nodes: Node[] = [];
    this.walkTree((node: Node) => {
      if (node.isKind(kind)) {
        nodes.push(node);
      }
    });
    return nodes;
  }


  /**
   * @override
   */
  public walkTree(func: (node: Node, data?: any) => void, data?: any) {
    func(this, data);
    for (const child of this.childNodes) {
      if (child) {
        child.walkTree(func, data);
      }
    }
    return data;
  }

  /**
   * Simple string version for debugging, just to get the structure.
   */
  public toString() {
    return this.kind + '(' + this.childNodes.join(',') + ')';
  }

}

/*********************************************************/
/**
 *  The abstract EmptyNode class
 */

export abstract class AbstractEmptyNode extends AbstractNode {
  /**
   *  We don't have children, so ignore these methods
   */

  /**
   * @override
   */
  public setChildren(_children: Node[]) {
  }

  /**
   * @override
   */
  public appendChild(child: Node) {
    return child;
  }

  /**
   * @override
   */
  public replaceChild(_newChild: Node, oldChild: Node) {
    return oldChild;
  }

  /**
   * @override
   */
  public childIndex(_node: Node) {
    return null as number;
  }

  /**
   * Don't step into children (there aren't any)
   *
   * @override
   */
  public walkTree(func: (node: Node, data?: any) => void, data?: any) {
    func(this, data);
    return data;
  }

  /**
   * Simple string version for debugging, just to get the structure.
   */
  public toString() {
    return this.kind;
  }

}
