/*************************************************************
 *
 *  Copyright (c) 2018 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  The DOMAdaptor interface and abstract class
 *
 * @author dpvc@mathjax.org (Davide Cervone)
 */

import {OptionList} from '../util/Options.js';

/**
 * The data for an attribute
 */
export type AttributeData = {
    name: string,
    value: string
};

/**
 * The data for an elements page-based bounding box
 */
export type PageBBox = {
    left: number,
    right: number,
    top: number,
    bottom: number
};


/*****************************************************************/
/**
 *  The interface for the DOMAdaptor
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export interface DOMAdaptor<N, T, D> {
    /**
     * Document in which the nodes are to be created
     */
    document: D;

    /**
     * @param {string} text    The serialized document to be parsed
     * @param {string} format  The format (e.g., 'text/html' or 'text/xhtml')
     * @return {D}             The parsed document
     */
    parse(text: string, format?: string): D;

    /**
     * @param {string} kind      The tag name of the HTML node to be created
     * @param {OptionList} def   The properties to set for the created node
     * @param {(N|T)[]} content  The child nodes for the created HTML node
     * @param {string} ns        The namespace in which to create the node
     * @return {N}               The generated HTML tree
     */
    node(kind: string, def?: OptionList, children?: (N | T)[], ns?: string): N;

    /**
     * @param {string} text   The text from which to create an HTML text node
     * @return {T}            The generated text node with the given text
     */
    text(text: string): T;

    /**
     * @param {D} doc   The document whose head is to be obtained
     * @return {N}      The document.head element
     */
    head(doc: D): N;

    /**
     * @param {D} doc   The document whose body is to be obtained
     * @return {N}      The document.body element
     */
    body(doc: D): N;

    /**
     * @param {D} doc   The document whose documentElement is to be obtained
     * @return {N}      The documentElement
     */
    root(doc: D): N;

    /**
     * @param {N} node        The node to search for tags
     * @param {string} name   The name of the tag to search for
     * @param {string} ns     The namespace to search in (or null for no namespace)
     * @return {N[]}          The list of tags found
     */
    tags(node: N, name: string, ns?: string): N[];

    /**
     * Get a list of containers (to be searched for math).  These can be
     *  specified by CSS selector, or as actual DOM elements or arrays of such.
     *
     * @param {(string | N | N[])[]} nodes  The array of items to make into a container list
     * @param {D} document                  The document in which to search
     * @return {N[]}                        The array of containers to search
     */
    getElements(nodes: (string | N | N[])[], document: D): N[];

    /**
     * @param {N|T} node  The HTML node whose parent is to be obtained
     * @return {N}        The parent node of the given one
     */
    parent(node: N | T): N;

    /**
     * @param {N} node     The HTML node to be appended to
     * @param {N|T} child  The node or text to be appended
     * @return {N|T}       The appended node
     */
    append(node: N, child: N | T): N | T;

    /**
     * @param {N|T} nchild  The node or text to be inserted
     * @param {N|T} ochild  The node or text where the new child is to be added before it
     */
    insert(nchild: N | T, ochild: N | T): void;

    /**
     * @param {N|T} child  The node or text to be removed from its parent
     * @return {N|T}       The removed node
     */
    remove(child: N | T): N | T;

    /**
     * @param {N|T} nnode  The node to replace with
     * @param {N|T} onode  The child to be replaced
     * @return {N|T}       The removed node
     */
    replace(nnode: N | T, onode: N | T): N | T;

    /**
     * @param {N} node   The HTML node to be cloned
     * @return {N}       The copied node
     */
    clone(node: N): N;

    /**
     * @param {T} node    The HTML text node to be split
     * @param {number} n  The index of the character where the split will occur
     */
    split(node: T, n: number): T;

    /**
     * @param {N|T} node   The HTML node whose sibling is to be obtained
     * @return {N|T}       The node following the given one (or null)
     */
    next(node: N | T): N | T;

    /**
     * @param {N|T} node   The HTML node whose sibling is to be obtained
     * @return {N|T}       The node preceding the given one (or null)
     */
    previous(node: N | T): N | T;

    /**
     * @param {N} node   The HTML node whose child is to be obtained
     * @return {N|T}     The first child of the given node (or null)
     */
    firstChild(node: N): N | T;

    /**
     * @param {N} node   The HTML node whose child is to be obtained
     * @return {N}       The last child of the given node (or null)
     */
    lastChild(node: N): N | T;

    /**
     * @param {N} node    The HTML node whose children are to be obtained
     * @return {(N|T)[]}  Array of children for the given node (not a live list)
     */
    childNodes(node: N): (N | T)[];

    /**
     * @param {N} node    The HTML node whose child is to be obtained
     * @param {number} i  The index of the child to return
     * @return {N|T}      The i-th child node of the given node (or null)
     */
    childNode(node: N, i: number): N | T;

    /**
     * @param {N | T} node   The HTML node whose tag or node name is to be obtained
     * @return {string}      The tag or node name of the given node
     */
    kind(node: N | T): string;

    /**
     * @param {N|T} node  The HTML node whose value is to be obtained
     * @return {string}   The value of the given node
     */
    value(node: N | T): string;

    /**
     * @param {N} node    The HTML node whose text content is to be obtained
     * @return {string}   The text content of the given node
     */
    textContent(node: N): string;

    /**
     * @param {N} node   The HTML node whose inner HTML string is to be obtained
     * @return {string}  The serialized content of the node
     */
    innerHTML(node: N): string;

    /**
     * @param {N} node   The HTML node whose outer HTML string is to be obtained
     * @return {string}  The serialized node and its content
     */
    outerHTML(node: N): string;

    /**
     * @param {N} node               The HTML node whose attribute is to be set
     * @param {string|number} name   The name of the attribute to set
     * @param {string} value         The new value of the attribute
     * @param {string=} ns           The namespace to use for the attribute
     */
    setAttribute(node: N, name: string, value: string | number, ns?: string): void;

    /**
     * @param {N} node           The HTML element whose attributes are to be set
     * @param {OptionList} def   The attributes to set on that node
     */
    setAttributes(node: N, def: OptionList): void;

    /**
     * @param {N} node        The HTML node whose attribute is to be obtained
     * @param {string} name   The name of the attribute to get
     * @return {string}       The value of the given attribute of the given node
     */
    getAttribute(node: N, name: string): string;

    /**
     * @param {N} node        The HTML node whose attribute is to be removed
     * @param {string} name   The name of the attribute to remove
     */
    removeAttribute(node: N, name: string): void;

    /**
     * @param {N} node        The HTML node whose attribute is to be tested
     * @param {string} name   The name of the attribute to test
     * @return {boolean}      True of the node has the given attribute defined
     */
    hasAttribute(node: N, name: string): boolean;

    /**
     * @param {N} node           The HTML node whose attributes are to be returned
     * @return {AttributeData[]} The list of attributes
     */
    allAttributes(node: N): AttributeData[];

    /**
     * @param {N} node        The HTML node whose class is to be augmented
     * @param {string} name   The class to be added
     */
    addClass(node: N, name: string): void;

    /**
     * @param {N} node        The HTML node whose class is to be changed
     * @param {string} name   The class to be removed
     */
    removeClass(node: N, name: string): void;

    /**
     * @param {N} node        The HTML node whose class is to be tested
     * @param {string} name   The class to test
     * @return {boolean}      True if the node has the given class
     */
    hasClass(node: N, name: string): void;

    /**
     * @param {N} node        The HTML node whose class list is needed
     * @return {string[]}     An array of the class names for this node
     */
    allClasses(node: N): string[];

    /**
     * @param {N} node        The HTML node whose style is to be changed
     * @param {string} name   The style to be set
     * @param {string} name   The new value of the style
     */
    setStyle(node: N, name: string, value: string): void;

    /**
     * @param {N} node        The HTML node whose style is to be obtained
     * @param {string} name   The style to be obtained
     * @return {string}       The value of the style
     */
    getStyle(node: N, name: string): string;

    /**
     * @param {N} node        The HTML node whose styles are to be returned
     * @return {string}       The cssText for the styles
     */
    allStyles(node: N): string;

    /**
     * @param {N} node        The HTML node whose font size is to be determined
     * @return {number}       The font size (in pixels) of the node
     */
    fontSize(node: N): number;

    /**
     * @param {N} node            The HTML node whose dimensions are to be determined
     * @param {number} em         The number of pixels in an em
     * @param {boolean} local     True if local coordinates are to be used in SVG elements
     * @return {[number, number]} The width and height (in ems) of the element
     */
    nodeSize(node: N, em?: number, local?: boolean): [number, number];


    /**
     * @param {N} node            The HTML node whose BBox is to be determined
     * @return {PageBBox}         BBox as {left, right, top, bottom} position on the page (in pixels)
     */
    nodeBBox(node: N): PageBBox;
}

/*****************************************************************/
/**
 *  Abstract DOMAdaptor class for creating HTML elements
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export abstract class AbstractDOMAdaptor<N, T, D> implements DOMAdaptor<N, T, D> {

    /**
     * The document in which the HTML nodes will be created
     */
    public document: D;

    /**
     * @param {D} document  The document in which the nodes will be created
     * @constructor
     */
    constructor(document: D = null) {
        this.document = document;
    }

    /**
     * @override
     */
    public abstract parse(text: string, format?: string): D;

    /**
     * @override
     */
    public node(kind: string, def: OptionList = {}, children: (N | T)[] = [], ns?: string) {
        const node = this.create(kind, ns);
        this.setAttributes(node, def);
        for (const child of children) {
            this.append(node, child);
        }
        return node as N;
    }

    /**
     * @param {string} kind  The type of the node to create
     * @param {string} ns    The optional namespace in which to create the node
     * @return {N}           The created node
     */
    protected abstract create(kind: string, ns?: string): N;

    /**
     * @override
     */
    public abstract text(text: string): T;

    /**
     * @param {N} node           The HTML element whose attributes are to be set
     * @param {OptionList} def   The attributes to set on that node
     */
    public setAttributes(node: N, def: OptionList) {
        if (def.style && typeof(def.style) !== 'string') {
            for (let key of Object.keys(def.style)) {
                this.setStyle(node, key.replace(/-([a-z])/g, (m, c) => c.toUpperCase()), def.style[key]);
            }
        }
        if (def.properties) {
            for (let key of Object.keys(def.properties)) {
                (node as OptionList)[key] = def.properties[key];
            }
        }
        for (let key of Object.keys(def)) {
            if ((key !== 'style' || typeof(def.style) === 'string') && key !== 'properties') {
                this.setAttribute(node, key, def[key]);
            }
        }
    }

    /**
     * @override
     */
    public abstract head(doc: D): N;

    /**
     * @override
     */
    public abstract body(doc: D): N;

    /**
     * @override
     */
    public abstract root(doc: D): N;

    /**
     * @override
     */
    public abstract tags(node: N, name: string, ns?: string): N[];

    /**
     * @override
     */
    public abstract getElements(nodes: (string | N | N[])[], document: D): N[];

    /**
     * @override
     */
    public abstract parent(node: N | T): N;

    /**
     * @override
     */
    public abstract append(node: N, child: N | T): N | T;

    /**
     * @override
     */
    public abstract insert(nchild: N | T, ochild: N | T): void;

    /**
     * @override
     */
    public abstract remove(child: N | T): N | T;

    /**
     * @override
     */
    public replace(nnode: N | T, onode: N | T) {
        this.insert(nnode, onode);
        this.remove(onode);
        return onode;
    }

    /**
     * @override
     */
    public abstract clone(node: N):  N;

    /**
     * @override
     */
    public abstract split(node: T, n: number): T;

    /**
     * @override
     */
    public abstract next(node: N | T): N | T;

    /**
     * @override
     */
    public abstract previous(node: N | T): N | T;

    /**
     * @override
     */
    public abstract firstChild(node: N): N | T;

    /**
     * @override
     */
    public abstract lastChild(node: N): N | T;

    /**
     * @override
     */
    public abstract childNodes(node: N): (N | T)[];

    /**
     * @override
     */
    public childNode(node: N, i: number) {
        return this.childNodes(node)[i];
    }

    /**
     * @override
     */
    public abstract kind(node: N | T): string;

    /**
     * @override
     */
    public abstract value(node: N | T): string;

    /**
     * @override
     */
    public abstract textContent(node: N): string;

    /**
     * @override
     */
    public abstract innerHTML(node: N): string;

    /**
     * @override
     */
    public abstract outerHTML(node: N): string;

    /**
     * @override
     */
    public abstract setAttribute(node: N, name: string, value: string, ns?: string): void;

    /**
     * @override
     */
    public abstract getAttribute(node: N, name: string): string;

    /**
     * @override
     */
    public abstract removeAttribute(node: N, name: string): void;

    /**
     * @override
     */
    public abstract hasAttribute(node: N, name: string): boolean;


    /**
     * @override
     */
    public abstract allAttributes(node: N): AttributeData[];

    /**
     * @override
     */
    public abstract addClass(node: N, name: string): void;

    /**
     * @override
     */
    public abstract removeClass(node: N, name: string): void;

    /**
     * @override
     */
    public abstract hasClass(node: N, name: string): boolean;

    /**
     * @override
     */
    public allClasses(node: N) {
        const classes = this.getAttribute(node, 'class');
        return (!classes ? [] as string[] :
                classes.replace(/  +/g, ' ').replace(/^ /, '').replace(/ $/, '').split(/ /));
    }

    /**
     * @override
     */
    public abstract setStyle(node: N, name: string, value: string): void;

    /**
     * @override
     */
    public abstract getStyle(node: N, name: string): string;

    /**
     * @override
     */
    public abstract allStyles(node: N): string;

    /**
     * @override
     */
    public abstract fontSize(node: N): number;

    /**
     * @override
     */
    public abstract nodeSize(node: N, em?: number, local?: boolean): [number, number];

    /**
     * @override
     */
    public abstract nodeBBox(node: N): PageBBox;

}
