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

import {CommonOutputJax} from './common/OutputJax.js';
import {OptionList} from '../util/Options.js';
import {MathDocument} from '../core/MathDocument.js';
import {MathItem} from '../core/MathItem.js';
import {MmlNode} from '../core/MmlTree/MmlNode.js';
import {SVGWrapper} from './svg/Wrapper.js';
import {SVGWrapperFactory} from './svg/WrapperFactory.js';
import {SVGFontData} from './svg/FontData.js';
import {TeXFont} from './svg/fonts/tex.js';
import {StyleList as CssStyleList} from './common/CssStyles.js';
import {FontCache} from './svg/FontCache.js';

export const SVGNS = "http://www.w3.org/2000/svg";
export const XLINKNS = 'http://www.w3.org/1999/xlink';

/*****************************************************************/
/**
 *  Implements the CHTML class (extends AbstractOutputJax)
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export class SVG<N, T, D> extends
CommonOutputJax<N, T, D, SVGWrapper<N, T, D>, SVGWrapperFactory<N, T, D>, SVGFontData, typeof SVGFontData> {

    public static NAME: string = 'SVG';
    public static OPTIONS: OptionList = {
        ...CommonOutputJax.OPTIONS,
        internalSpeechTitles: true,     // insert <title> tags with speech content
        titleID: 0,                     // initial id number to use for aria-labeledby titles
        fontCache: 'local',             // or 'global' or 'none'
        localID: null,                  // ID to use for local font cache (for single equation processing)
    };

    /**
     *  The default styles for SVG
     */
    public static commonStyles: CssStyleList = {
        'mjx-container[jax="SVG"] > svg': {
            'overflow': 'visible'
        },
        'mjx-container[jax="SVG"] > svg a': {
            fill: 'blue', stroke: 'blue'
        }
    };

    /**
     * The ID for the SVG element that stores the cached font paths
     */
    public static FONTCACHEID = 'MJX-SVG-global-cache';

    /**
     * The ID for the stylesheet element for the styles for the SVG output
     */
    public static STYLESHEETID = 'MJX-SVG-styles';

    /**
     * Stores the CHTMLWrapper factory
     */
    public factory: SVGWrapperFactory<N, T, D>;

    /**
     * Stores the information about the cached character glyphs
     */
    public fontCache: FontCache<N, T, D>;

    /**
     * Minimum width for tables with labels,
     * and shift and align for main equation
     */
    public minwidth: number = 0;
    public shift: number = 0;

    /**
     * The container element for the math
     */
    public container: N;

    /**
     * @override
     * @constructor
     */
    constructor(options: OptionList = null) {
        super(options, SVGWrapperFactory as any, TeXFont);
        this.fontCache = new FontCache(this);
    }

    /**
     * @override
     */
    public initialize() {
        if (this.options.fontCache === 'global') {
            this.fontCache.clearCache();
        }
    }

    /**
     * Clear the font cache (use for resetting the global font cache)
     */
    public clearFontCache() {
        this.fontCache.clearCache();
    }

    /**
     * Don't set the scaling factor for the container (that is handled by the
     *   viewBox and height and width settings)
     *
     * @override
     */
    protected setScale(node: N) {
    }

    /**
     * @override
     */
    public escaped(math: MathItem<N, T, D>, html: MathDocument<N, T, D>) {
        this.setDocument(html);
        return this.html('span', {}, [this.text(math.math)]);
    }

    /**
     * @override
     */
    public styleSheet(html: MathDocument<N, T, D>) {
        const sheet = super.styleSheet(html);
        this.adaptor.setAttribute(sheet, 'id', SVG.STYLESHEETID);
        return sheet;
    }

    /**
     * @override
     */
    public pageElements(html: MathDocument<N, T, D>) {
        if (this.options.fontCache === 'global' && !this.findCache(html)) {
            return this.svg('svg', {id: SVG.FONTCACHEID, style: {display: 'none'}}, [this.fontCache.getCache()]);
        }
        return null as N;
    }

    /**
     * Checks if there is already a font-cache element in the page
     *
     * @param {MathDocument} html   The document to search
     * @return {boolean}            True if a font cache already exists in the page
     */
    protected findCache(html: MathDocument<N, T, D>) {
        const adaptor = this.adaptor;
        const svgs = adaptor.tags(adaptor.body(html.document), 'svg');
        for (let i = svgs.length - 1; i >= 0; i--) {
            if (this.adaptor.getAttribute(svgs[i], 'id') === SVG.FONTCACHEID) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param {MmlNode} math  The MML node whose SVG is to be produced
     * @param {N} parent      The HTML node to contain the SVG
     */
    protected processMath(math: MmlNode, parent: N) {
        //
        // Cache the container (tooltips process into separate containers)
        //
        const container = this.container;
        this.container = parent;
        //
        //  Get the wrapped math element and the SVG container
        //  Then typeset the math into the SVG
        //
        const wrapper = this.factory.wrap(math);
        const [svg, g] = this.createRoot(wrapper);
        this.typesetSVG(wrapper, svg, g);
        //
        //  Put back the original container
        //
        this.container = container;
    }

    /**
     * @param {SVGWrapper} wrapper   The wrapped math to process
     * @return {[N, N]}              The svg and g nodes for the math
     */
    protected createRoot(wrapper: SVGWrapper<N, T, D>) {
        const {w, h, d, pwidth} = wrapper.getBBox();
        const W = Math.max(w, .001); // make sure we are at least one unit wide (needed for e.g. \llap)
        //
        //  The container that flips the y-axis and sets the colors to inherit from the surroundings
        //
        const g = this.svg('g', {
            stroke: 'currentColor', fill: 'currentColor',
            'stroke-width': 0, transform: 'matrix(1 0 0 -1 0 0)'
        }) as N;
        //
        //  The svg element with its viewBox, size and alignment
        //
        const adaptor = this.adaptor;
        const svg = adaptor.append(this.container, this.svg('svg', {
            xmlns: SVGNS,
            width: this.ex(W), height: this.ex(h + d),
            role: 'img', focusable: false,
            style: {'vertical-align': this.ex(-d)},
            viewBox: [0, this.fixed(-h * 1000, 1), this.fixed(W * 1000, 1), this.fixed((h + d) * 1000, 1)].join(' ')
        }, [g])) as N;
        if (W === .001) {
            adaptor.setAttribute(svg, 'preserveAspectRatio', 'xMidYMid slice');
            if (w < 0) {
                adaptor.setStyle(this.container, 'margin-right', this.ex(w));
            }
        }
        if (pwidth) {
            //
            // Use width 100% with no viewbox, and instead scale and translate to achieve the same result
            //
            adaptor.setAttribute(svg, 'width', pwidth);
            adaptor.removeAttribute(svg, 'viewBox');
            const scale = wrapper.metrics.ex / (this.font.params.x_height * 1000);
            adaptor.setAttribute(g, 'transform', 'matrix(1 0 0 -1 0 0) scale(' +
                                 this.fixed(scale, 6) + ') translate(0, ' + this.fixed(-h * 1000, 1) + ')');
        }
        if (this.options.fontCache !== 'none') {
            adaptor.setAttribute(svg, 'xmlns:xlink', XLINKNS);
        }
        return [svg, g];
    }

    /**
     * Typeset the math and add minwidth (from mtables), or set the alignment and indentation
     * of the finalized expression.
     *
     * @param {SVGWrapper} wrapper   The wrapped math to typeset
     * @param {N} svg                The main svg element for the typeet math
     * @param {N} g                  The group in which the math is typeset
     */
    protected typesetSVG(wrapper: SVGWrapper<N, T, D>, svg: N, g: N) {
        const adaptor = this.adaptor;
        //
        //  Typeset the math and add minWidth (from mtables), or set the alignment and indentation
        //    of the finalized expression
        //
        this.minwidth = this.shift = 0;
        if (this.options.fontCache === 'local') {
            this.fontCache.clearCache();
            this.fontCache.useLocalID(this.options.localID);
            adaptor.insert(this.fontCache.getCache(), g);
        }
        wrapper.toSVG(g);
        this.fontCache.clearLocalID();
        if (this.minwidth) {
            adaptor.setStyle(svg, 'minWidth', this.ex(this.minwidth));
        } else if (this.shift) {
            const align = adaptor.getAttribute(this.container, 'justify') || 'center';
            this.setIndent(svg, align, this.shift);
        }
    }

    /**
     * @param {N} svg         The svg 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(svg: N, align: string, shift: number) {
        if (align === 'center' || align === 'left') {
            this.adaptor.setStyle(svg, 'margin-left', this.ex(shift));
        }
        if (align === 'center' || align === 'right') {
            this.adaptor.setStyle(svg, 'margin-right', this.ex(-shift));
        }
    }

    /**
     * @param {number} m  A number to be shown in ex
     * @return {string}   The number with units of ex
     */
    ex(m: number) {
        m /= this.font.params.x_height;
        return (Math.abs(m) < .001 ? '0' : m.toFixed(3).replace(/\.?0+$/, '') + 'ex');
    }

    /**
     * @param {string} kind             The kind of node to create
     * @param {OptionList} properties   The properties to set for the element
     * @param {(N|T)[]} children            The child nodes for this node
     * @return {N}                      The newly created node in the SVG namespace
     */
    svg(kind: string, properties: OptionList = {}, children: (N|T)[] = []) {
        return this.html(kind, properties, children, SVGNS);
    }

    /**
     * @param {string} text      The text to be displayed
     * @param {string} variant   The name of the variant for the text
     * @return {N}               The text element containing the text
     */
    public unknownText(text: string, variant: string) {
        const metrics = this.math.metrics;
        const scale = this.font.params.x_height / metrics.ex * metrics.em * 1000;
        const svg = this.svg('text', {
            'data-variant': variant,
            transform: 'matrix(1 0 0 -1 0 0)', 'font-size': this.fixed(scale, 1) + 'px'
        }, [this.text(text)]);
        const adaptor = this.adaptor;
        if (variant !== '-explicitFont') {
            const [family, italic, bold] = this.font.getCssFont(variant);
            adaptor.setAttribute(svg, 'font-family', family);
            if (italic) {
                adaptor.setAttribute(svg, 'font-style', 'italic');
            }
            if (bold) {
                adaptor.setAttribute(svg, 'font-weight', 'bold');
            }
        }
        return svg;
    }

    /**
     * Measure the width of a text element by placing it in the page
     *  and looking up its size (fake the height and depth, since we can't measure that)
     *
     * @param {N} text         The text element to measure
     * @return {Object}        The width, height and depth for the text
     */
    public measureTextNode(text: N) {
        const adaptor = this.adaptor;
        text = adaptor.clone(text);
        adaptor.removeAttribute(text, 'transform');
        const ex = this.fixed(this.font.params.x_height * 1000, 1);
        const svg = this.svg('svg', {
            position: 'absolute', visibility: 'hidden',
            width: '1ex', height: '1ex',
            viewBox: [0, 0, ex, ex].join(' ')
        }, [text]);
        adaptor.append(adaptor.body(adaptor.document), svg);
        let w = adaptor.nodeSize(text, 1000, true)[0];
        adaptor.remove(svg);
        return {w: w, h: .75, d: .25};
    }

}
