/*************************************************************
 *
 *  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  Implements the CHTMLWrapper class
 *
 * @author dpvc@mathjax.org (Davide Cervone)
 */

import {PropertyList} from '../../core/Tree/Node.js';
import {MmlNode, TextNode, AbstractMmlNode, AttributeList, indentAttributes} from '../../core/MmlTree/MmlNode.js';
import {OptionList} from '../../util/Options.js';
import * as LENGTHS from '../../util/lengths.js';
import {CommonWrapper, AnyWrapperClass, Constructor, StringMap} from '../common/Wrapper.js';
import {CHTML} from '../chtml.js';
import {CHTMLWrapperFactory} from './WrapperFactory.js';
import {CHTMLmo} from './Wrappers/mo.js';
import {BBox} from './BBox.js';
import {CHTMLFontData, CHTMLCharOptions, CHTMLDelimiterData} from './FontData.js';

export {Constructor, StringMap} from '../common/Wrapper.js';

/*****************************************************************/

/**
 * Some standard sizes to use in predefind CSS properties
 */
export const FONTSIZE: StringMap = {
    '70.7%': 's',
    '70%': 's',
    '50%': 'ss',
    '60%': 'Tn',
    '85%': 'sm',
    '120%': 'lg',
    '144%': 'Lg',
    '173%': 'LG',
    '207%': 'hg',
    '249%': 'HG'
};

export const SPACE: StringMap = {
    [LENGTHS.em(2/18)]: '1',
    [LENGTHS.em(3/18)]: '2',
    [LENGTHS.em(4/18)]: '3',
    [LENGTHS.em(5/18)]: '4',
    [LENGTHS.em(6/18)]: '5'
};

/**
 * Needed to access node.style[id] using variable id
 */
interface CSSStyle extends CSSStyleDeclaration {
    [id: string]: string | Function | number | CSSRule;
}

/**
 * Shorthand for making a CHTMLWrapper constructor
 */
export type CHTMLConstructor<N, T, D> = Constructor<CHTMLWrapper<N, T, D>>;


/*****************************************************************/
/**
 *  The type of the CHTMLWrapper class (used when creating the wrapper factory for this class)
 */
export interface CHTMLWrapperClass<N, T, D> extends AnyWrapperClass {

    kind: string;

    /**
     * If true, this causes a style for the node type to be generated automatically
     * that sets display:inline-block (as needed for the output for MmlNodes).
     */
    autoStyle: boolean;

    /**
     * True when an instance of this class has been typeset
     * (used to control whether the styles for this class need to be output)
     */
    used: boolean;

}

/*****************************************************************/
/**
 *  The base CHTMLWrapper class
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export class CHTMLWrapper<N, T, D> extends
CommonWrapper<
    CHTML<N, T, D>,
    CHTMLWrapper<N, T, D>,
    CHTMLWrapperClass<N, T, D>,
    CHTMLCharOptions,
    CHTMLDelimiterData,
    CHTMLFontData
> {

    public static kind: string = 'unknown';

    /**
     * If true, this causes a style for the node type to be generated automatically
     * that sets display:inline-block (as needed for the output for MmlNodes).
     */
    public static autoStyle = true;

    /**
     * True when an instance of this class has been typeset
     * (used to control whether the styles for this class need to be output)
     */
    public static used: boolean = false;

    /**
     * The factory used to create more CHTMLWrappers
     */
    protected factory: CHTMLWrapperFactory<N, T, D>;

    /**
     * The parent and children of this node
     */
    public parent: CHTMLWrapper<N, T, D>;
    public childNodes: CHTMLWrapper<N, T, D>[];

    /**
     * The HTML element generated for this wrapped node
     */
    public chtml: N = null;

    /*******************************************************************/

    /**
     * Create the HTML for the wrapped node.
     *
     * @param {N} parent  The HTML node where the output is added
     */
    public toCHTML(parent: N) {
        const chtml = this.standardCHTMLnode(parent);
        for (const child of this.childNodes) {
            child.toCHTML(chtml);
        }
    }

    /*******************************************************************/

    /**
     * Create the standard CHTML element for the given wrapped node.
     *
     * @param {N} parent  The HTML element in which the node is to be created
     * @returns {N}  The root of the HTML tree for the wrapped node's output
     */
    protected standardCHTMLnode(parent: N) {
        this.markUsed();
        const chtml = this.createCHTMLnode(parent);
        this.handleStyles();
        this.handleVariant();
        this.handleScale();
        this.handleColor();
        this.handleSpace();
        this.handleAttributes();
        this.handlePWidth();
        return chtml;
    }

    /**
     * Mark this class as having been typeset (so its styles will be output)
     */
    public markUsed() {
        (this.constructor as CHTMLWrapperClass<N, T, D>).used = true;
    }

    /**
     * @param {N} parent  The HTML element in which the node is to be created
     * @returns {N}  The root of the HTML tree for the wrapped node's output
     */
    protected createCHTMLnode(parent: N) {
        const href = this.node.attributes.get('href');
        if (href) {
            parent = this.adaptor.append(parent, this.html('a', {href: href})) as N;
        }
        this.chtml = this.adaptor.append(parent, this.html('mjx-' + this.node.kind)) as N;
        return this.chtml;
    }

    /**
     * Set the CSS styles for the chtml element
     */
    protected handleStyles() {
        if (!this.styles) return;
        const styles = this.styles.cssText;
        if (styles) {
            this.adaptor.setAttribute(this.chtml, 'style', styles);
            const family = this.styles.get('font-family');
            if (family) {
                this.adaptor.setStyle(this.chtml, 'font-family', 'MJXZERO, ' + family);
            }
        }
    }

    /**
     * Set the CSS for the math variant
     */
    protected handleVariant() {
        if (this.node.isToken && this.variant !== '-explicitFont') {
            this.adaptor.setAttribute(this.chtml, 'class',
                                    (this.font.getVariant(this.variant) || this.font.getVariant('normal')).classes);
        }
    }

    /**
     * Set the (relative) scaling factor for the node
     */
    protected handleScale() {
        this.setScale(this.chtml, this.bbox.rscale);
    }

    /**
     * @param {N} chtml  The HTML node to scale
     * @param {number} rscale      The relatie scale to apply
     * @return {N}       The HTML node (for chaining)
     */
    protected setScale(chtml: N, rscale: number) {
        const scale = (Math.abs(rscale - 1) < .001 ? 1 : rscale);
        if (chtml && scale !== 1) {
            const size = this.percent(scale);
            if (FONTSIZE[size]) {
                this.adaptor.setAttribute(chtml, 'size', FONTSIZE[size]);
            } else {
                this.adaptor.setStyle(chtml, 'fontSize', size);
            }
        }
        return chtml;
    }

    /**
     * Add the proper spacing
     */
    protected handleSpace() {
        for (const data of [[this.bbox.L, 'space',  'marginLeft'],
                            [this.bbox.R, 'rspace', 'marginRight']]) {
            const [dimen, name, margin] = data as [number, string, string];
            if (dimen) {
                const space = this.em(dimen);
                if (SPACE[space]) {
                    this.adaptor.setAttribute(this.chtml, name, SPACE[space]);
                } else {
                    this.adaptor.setStyle(this.chtml, margin, space);
                }
            }
        }
    }

    /**
     * Add the foreground and background colors
     * (Only look at explicit attributes, since inherited ones will
     *  be applied to a parent element, and we will inherit from that)
     */
    protected handleColor() {
        const attributes = this.node.attributes;
        const mathcolor = attributes.getExplicit('mathcolor') as string;
        const color = attributes.getExplicit('color') as string;
        const mathbackground = attributes.getExplicit('mathbackground') as string;
        const background = attributes.getExplicit('background') as string;
        if (mathcolor || color) {
            this.adaptor.setStyle(this.chtml, 'color', mathcolor || color);
        }
        if (mathbackground || background) {
            this.adaptor.setStyle(this.chtml, 'backgroundColor', mathbackground || background);
        }
    }

    /**
     * Copy RDFa, aria, and other tags from the MathML to the CHTML output nodes.
     * Don't copy those in the skipAttributes list, or anything that already exists
     * as a property of the node (e.g., no "onlick", etc.).  If a name in the
     * skipAttributes object is set to false, then the attribute WILL be copied.
     * Add the class to any other classes already in use.
     */
    protected handleAttributes() {
        const attributes = this.node.attributes;
        const defaults = attributes.getAllDefaults();
        const skip = CHTMLWrapper.skipAttributes;
        for (const name of attributes.getExplicitNames()) {
            if (skip[name] === false || (!(name in defaults) && !skip[name] &&
                                         !this.adaptor.hasAttribute(this.chtml, name))) {
                this.adaptor.setAttribute(this.chtml, name, attributes.getExplicit(name) as string);
            }
        }
        if (attributes.get('class')) {
            this.adaptor.addClass(this.chtml, attributes.get('class') as string);
        }
    }

    /**
     * Handle the attributes needed for percentage widths
     */
    protected handlePWidth() {
        if (this.bbox.pwidth) {
            if (this.bbox.pwidth === BBox.fullWidth) {
                this.adaptor.setAttribute(this.chtml, 'width', 'full');
            } else {
                this.adaptor.setStyle(this.chtml, 'width', this.bbox.pwidth);
            }
        }
    }

    /*******************************************************************/

    /**
     * @param {N} chtml       The HTML node whose indentation is to be adjusted
     * @param {string} align  The alignment for the node
     * @param {number} shift  The indent (positive or negative) for the node
     */
    protected setIndent(chtml: N, align: string, shift: number) {
        const adaptor = this.adaptor;
        if (align === 'center' || align === 'left') {
            const L = this.getBBox().L;
            adaptor.setStyle(chtml, 'margin-left', this.em(shift + L));
        }
        if (align === 'center' || align === 'right') {
            const R = this.getBBox().R;
            adaptor.setStyle(chtml, 'margin-right', this.em(-shift + R));
        }
    }

    /*******************************************************************/
    /**
     * For debugging
     */

    public drawBBox() {
        let {w, h, d, R}  = this.getBBox();
        const box = this.html('mjx-box', {style: {
            opacity: .25, 'margin-left': this.em(-w - R)
        }}, [
            this.html('mjx-box', {style: {
                height: this.em(h),
                width: this.em(w),
                'background-color': 'red'
            }}),
            this.html('mjx-box', {style: {
                height: this.em(d),
                width: this.em(w),
                'margin-left': this.em(-w),
                'vertical-align': this.em(-d),
                'background-color': 'green'
            }})
        ] as N[]);
        const node = this.chtml || this.parent.chtml;
        const size = this.adaptor.getAttribute(node, 'size');
        if (size) {
            this.adaptor.setAttribute(box, 'size', size);
        }
        const fontsize = this.adaptor.getStyle(node, 'fontSize');
        if (fontsize) {
            this.adaptor.setStyle(box, 'fontSize', fontsize);
        }
        this.adaptor.append(this.adaptor.parent(node), box);
        this.adaptor.setStyle(node, 'backgroundColor', '#FFEE00');
    }

    /*******************************************************************/
    /*
     * Easy access to some utility routines
     */

    /**
     * @param {string} type      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
     * @return {N}               The generated HTML tree
     */
    public html(type: string, def: OptionList = {}, content: (N | T)[] = []) {
        return this.jax.html(type, def, content);
    }

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

    /**
     * @override
     */
    protected createMo(text: string): CHTMLmo<N, T, D> {
        return super.createMo(text) as CHTMLmo<N, T, D>;
    }

    /**
     * @override
     */
    public coreMO(): CHTMLmo<N, T, D> {
        return super.coreMO() as CHTMLmo<N, T, D>;
    }

    /**
     * @param {number} n  A unicode code point to be converted to a character className reference.
     * @return {string}  The className for the character
     */
    protected char(n: number) {
        return this.font.charSelector(n).substr(1);
    }

}
