/*************************************************************
 *
 *  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 utilities for notations for menclose elements
 *
 * @author dpvc@mathjax.org (Davide Cervone)
 */

import {BBox} from './BBox.js';
import {AnyWrapper} from './Wrapper.js';
import {CommonMenclose} from './Wrappers/menclose.js';

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

export const ARROWX = 4, ARROWDX = 1, ARROWY = 2;  // default relative arrowhead values

export const THICKNESS = .067;  // default rule thickness
export const PADDING = .2;      // default padding

export const SOLID = THICKNESS + 'em solid';  // a solid border

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

/**
 * Shorthand for CommonMenclose
 */
export type Menclose = CommonMenclose<any, any, any>;

/**
 * The functions used for notation definitions
 *
 * @templare N  The DOM node class
 */
export type Renderer<W extends AnyWrapper, N> = (node: W, child: N) => void;
export type BBoxExtender<W extends AnyWrapper> = (node: W) => number[];
export type BBoxBorder<W extends AnyWrapper> = (node: W) => number[];
export type Initializer<W extends AnyWrapper> = (node: W) => void;

/**
 * The definition of a notation
 *
 * @template W  The menclose wrapper class
 * @templare N  The DOM node class
 */
export type NotationDef<W extends AnyWrapper, N> = {
    renderer: Renderer<W, N>;  // renders the DOM nodes for the notation
    bbox: BBoxExtender<W>;     // gives the offsets to the child bounding box: [top, right, bottom, left]
    border?: BBoxBorder<W>;    // gives the amount of the bbox offset that is due to borders on the child
    renderChild?: boolean;     // true if the notation is used to render the child directly (e.g., radical)
    init?: Initializer<W>;     // function to be called during wrapper construction
    remove?: string;           // list of notations that are suppressed by this one
};

/**
 * For defining notation maps
 *
 * @template W  The menclose wrapper class
 * @templare N  The DOM node class
 */
export type DefPair<W extends AnyWrapper, N> = [string, NotationDef<W, N>];
export type DefList<W extends AnyWrapper, N> = Map<string, NotationDef<W, N>>;

/**
 * The list of notations for an menclose element
 *
 * @template W  The menclose wrapper class
 * @templare N  The DOM node class
 */
export type List<W extends AnyWrapper, N> = {[notation: string]: NotationDef<W, N>};

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

/**
 * The names and indices of sides for borders, padding, etc.
 */
export const sideIndex = {top: 0, right: 1, bottom: 2, left: 3};
export type Side = keyof typeof sideIndex;
export const sideNames = Object.keys(sideIndex) as Side[];

/**
 * Common BBox and Border functions
 */
export const fullBBox = ((node) => new Array(4).fill(node.thickness + node.padding)) as BBoxExtender<Menclose>;
export const fullPadding = ((node) => new Array(4).fill(node.padding)) as BBoxExtender<Menclose>;
export const fullBorder = ((node) => new Array(4).fill(node.thickness)) as BBoxBorder<Menclose>;

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

/**
 * The length of an arrowhead
 */
export const arrowHead = (node: Menclose) => {
    return Math.max(node.padding, node.thickness * (node.arrowhead.x + node.arrowhead.dx + 1));
};

/**
 * Adjust short bbox for tall arrow heads
 */
export const arrowBBoxHD = (node: Menclose, TRBL: number[]) => {
    if (node.childNodes[0]) {
        const {h, d} = node.childNodes[0].getBBox();
        TRBL[0] = TRBL[2] = Math.max(0, node.thickness * node.arrowhead.y - (h + d) / 2);
    }
    return TRBL;
}

/**
 * Adjust thin bbox for wide arrow heads
 */
export const arrowBBoxW = (node: Menclose, TRBL: number[]) => {
    if (node.childNodes[0]) {
        const {w} = node.childNodes[0].getBBox();
        TRBL[1] = TRBL[3] = Math.max(0, node.thickness * node.arrowhead.y - w / 2);
    }
    return TRBL;
}

/**
 * The data for horizontal and vertical arrow notations
 *   [angle, double, isVertical, remove]
 */
export const arrowDef = {
    up:        [-Math.PI / 2, false, true,  'verticalstrike'],
    down:      [ Math.PI / 2, false, true,  'verticakstrike'],
    right:     [ 0,           false, false, 'horizontalstrike'],
    left:      [ Math.PI,     false, false, 'horizontalstrike'],
    updown:    [ Math.PI / 2, true,  true,  'verticalstrike uparrow downarrow'],
    leftright: [ 0,           true,  false, 'horizontalstrike leftarrow rightarrow']
} as {[name: string]: [number, boolean, boolean, string]};

/**
 * The data for diagonal arrow notations
 *   [c, pi, double, remove]
 */
export const diagonalArrowDef = {
    updiagonal:         [-1, 0,       false, 'updiagonalstrike northeastarrow'],
    northeast:          [-1, 0,       false, 'updiagonalstrike updiagonalarrow'],
    southeast:          [ 1, 0,       false, 'downdiagonalstrike'],
    northwest:          [ 1, Math.PI, false, 'downdiagonalstrike'],
    southwest:          [-1, Math.PI, false, 'updiagonalstrike'],
    northeastsouthwest: [-1, 0,       true,  'updiagonalstrike northeastarrow updiagonalarrow southwestarrow'],
    northwestsoutheast: [ 1, 0,       true,  'downdiagonalstrike northwestarrow southeastarrow']
} as {[name: string]: [number, number, boolean, string]};

/**
 * The BBox functions for horizontal and vertical arrows
 */
export const arrowBBox = {
    up:    (node) => arrowBBoxW(node, [arrowHead(node), 0, node.padding, 0]),
    down:  (node) => arrowBBoxW(node, [node.padding, 0, arrowHead(node), 0]),
    right: (node) => arrowBBoxHD(node, [0, arrowHead(node), 0, node.padding]),
    left:  (node) => arrowBBoxHD(node, [0, node.padding, 0, arrowHead(node)]),
    updown:    (node) => arrowBBoxW(node, [arrowHead(node), 0, arrowHead(node), 0]),
    leftright: (node) => arrowBBoxHD(node, [0, arrowHead(node), 0, arrowHead(node)])
} as {[name: string]: BBoxExtender<Menclose>};

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

/**
 * @param {Renderer} render     The function for adding the border to the node
 * @return {string => DefPair}  The function returingn the notation definition
 *                              for the notation having a line on the given side
 */
export const CommonBorder = <W extends Menclose, N>(render: Renderer<W, N>) => {
    /**
     * @param {string} side   The side on which a border should appear
     * @return {DefPair}      The notation definition for the notation having a line on the given side
     */
    return (side: Side) => {
        const i = sideIndex[side];
        return [side, {
            //
            // Add the border to the main child object
            //
            renderer: render,
            //
            // Indicate the extra space on the given side
            //
            bbox: (node) => {
                const bbox = [0, 0, 0, 0];
                bbox[i] = node.thickness + node.padding;
                return bbox;
            },
            //
            // Indicate the border on the given side
            //
            border: (node) => {
                const bbox = [0, 0, 0, 0];
                bbox[i] = node.thickness;
                return bbox;
            }
        }] as DefPair<W, N>;
    }
};

/**
 * @param {Renderer} render                    The function for adding the borders to the node
 * @return {(sring,string,string) => DefPair}  The function returning the notation definition
 *                                             for the notation having lines on two sides
 */
export const CommonBorder2 = <W extends Menclose, N>(render: Renderer<W, N>) => {
    /**
     * @param {string} name    The name of the notation to define
     * @param {string} side1   The first side to get a border
     * @param {string} side2   The second side to get a border
     * @return {DefPair}       The notation definition for the notation having lines on two sides
     */
    return (name: string, side1: Side, side2: Side) => {
        const i1 = sideIndex[side1];
        const i2 = sideIndex[side2];
        return [name, {
            //
            // Add the border along the given sides
            //
            renderer: render,
            //
            // Mark the extra space along the two sides
            //
            bbox: (node) => {
                const t = node.thickness + node.padding;
                const bbox = [0, 0, 0, 0];
                bbox[i1] = bbox[i2] = t;
                return bbox;
            },
            //
            // Indicate the border on the two sides
            //
            border: (node) => {
                const bbox = [0, 0, 0, 0];
                bbox[i1] = bbox[i2] = node.thickness;
                return bbox;
            },
            //
            // Remove the single side notations, if present
            //
            remove: side1 + ' ' + side2
        }] as DefPair<W, N>;
    }
};

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

/**
 * @param {string => Renderer} render      The function for adding the strike to the node
 * @return {(string, number) => DefPair}   The function returning the notation definition for the diagonal strike
 */
export const CommonDiagonalStrike = <W extends Menclose, N>(render: (sname: string) => Renderer<W, N>) => {
    /**
     * @param {string} name  The name of the diagonal strike to define
     * @return {DefPair}     The notation definition for the diagonal strike
     */
    return (name: string) => {
        const cname = 'mjx-' + name.charAt(0) + 'strike';
        return [name + 'diagonalstrike', {
            //
            // Find the angle and width from the bounding box size and create the diagonal line
            //
            renderer: render(cname),
            //
            //  Add padding all around
            //
            bbox: fullBBox
        }] as DefPair<W, N>;
    }
};

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

/**
 * @param {Renderer} render     The function to add the arrow to the node
 * @return {string => DefPair}  The funciton returning the notation definition for the diagonal arrow
 */
export const CommonDiagonalArrow = <W extends Menclose, N>(render: Renderer<W, N>) => {
    /**
     * @param {string} name   The name of the diagonal arrow to define
     * @return {DefPair}      The notation definition for the diagonal arrow
     */
    return (name: string) => {
        const [c, pi, double, remove] = diagonalArrowDef[name];
        return [name + 'arrow', {
            //
            // Find the angle and width from the bounding box size and create
            //   the arrow from them and the other arrow data
            //
            renderer: (node, child) => {
                const {a, W} = node.arrowData();
                const arrow = node.arrow(W, c * (a - pi), double);
                render(node, arrow);
            },
            //
            // Add space for the arrowhead all around
            //
            bbox: (node) => {
                const {a, x, y} = node.arrowData();
                const [ax, ay, adx] = [node.arrowhead.x, node.arrowhead.y, node.arrowhead.dx];
                const [b, ar] = node.getArgMod(ax + adx, ay);
                const dy = y + (b > a ? node.thickness * ar * Math.sin(b - a) : 0);
                const dx = x + (b > Math.PI / 2 - a ? node.thickness * ar * Math.sin(b + a - Math.PI / 2) : 0);
                return [dy, dx, dy, dx];
            },
            //
            // Remove redundant notations
            //
            remove: remove
        }] as DefPair<W, N>;
    }
};

/**
 * @param {Renderer} render     The function to add the arrow to the node
 * @return {string => DefPair}  The function returning the notation definition for the arrow
 */
export const CommonArrow = <W extends Menclose, N>(render: Renderer<W, N>) => {
    /**
     * @param {string} name   The name of the horizontal or vertical arrow to define
     * @return {DefPair}      The notation definition for the arrow
     */
    return (name: string) => {
        const [angle, double, isVertical, remove] = arrowDef[name];
        return [name + 'arrow', {
            //
            // Get the arrow height and depth from the bounding box and the arrow direction
            //   then create the arrow from that and the other data
            //
            renderer: (node, child) => {
                const {w, h, d} = node.getBBox();
                const [W, H] = (isVertical ? [h + d, w] : [w, h + d]);
                const arrow = node.arrow(W, angle, double);
                render(node, arrow);
            },
            //
            // Add the padding to the proper sides
            //
            bbox: arrowBBox[name],
            //
            // Remove redundant notations
            //
            remove: remove
        }] as DefPair<W, N>;
    }
};
