/*************************************************************
 *
 *  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
     * @return {Node}  The newly created node
     *
     * @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;
    }

}
