/**
 * This file contains information about the options that the Parser carries
 * around with it while parsing. Data is held in an `Options` object, and when
 * recursing, a new `Options` object can be created with the `.with*` and
 * `.reset` functions.
 */

import {getGlobalMetrics} from "./fontMetrics";
import type {StyleInterface} from "./Style";
import type {FontMetrics, MathFont, TextFont, FontWeight, FontShape} from "./types/fonts";

const sizeStyleMap = [
    // Each element contains [textsize, scriptsize, scriptscriptsize].
    // The size mappings are taken from TeX with \normalsize=10pt.
    [1, 1, 1],    // size1: [5, 5, 5]              \tiny
    [2, 1, 1],    // size2: [6, 5, 5]
    [3, 1, 1],    // size3: [7, 5, 5]              \scriptsize
    [4, 2, 1],    // size4: [8, 6, 5]              \footnotesize
    [5, 2, 1],    // size5: [9, 6, 5]              \small
    [6, 3, 1],    // size6: [10, 7, 5]             \normalsize
    [7, 4, 2],    // size7: [12, 8, 6]             \large
    [8, 6, 3],    // size8: [14.4, 10, 7]          \Large
    [9, 7, 6],    // size9: [17.28, 12, 10]        \LARGE
    [10, 8, 7],   // size10: [20.74, 14.4, 12]     \huge
    [11, 10, 9],  // size11: [24.88, 20.74, 17.28] \HUGE
];

const sizeMultipliers = [
    // fontMetrics.js:getGlobalMetrics also uses size indexes, so if
    // you change size indexes, change that function.
    0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.2, 1.44, 1.728, 2.074, 2.488,
];

const sizeAtStyle = function(size: number, style: StyleInterface): number {
    return style.size < 2 ? size : sizeStyleMap[size - 1][style.size - 1];
};

export type OptionsData = {
    style: StyleInterface;
    color?: string | undefined;
    size?: number;
    textSize?: number;
    phantom?: boolean;
    font?: MathFont;
    fontFamily?: TextFont;
    fontWeight?: FontWeight;
    fontShape?: FontShape;
    sizeMultiplier?: number;
    maxSize: number;
    minRuleThickness: number;
};

/**
 * This is the main options class. It contains the current style, size, color,
 * and font.
 *
 * Options objects should not be modified. To create a new Options with
 * different properties, call a `.having*` method.
 */
class Options {
    style: StyleInterface;
    color: string | undefined;
    size: number;
    textSize: number;
    phantom: boolean;
    // A font family applies to a group of fonts (i.e. SansSerif), while a font
    // represents a specific font (i.e. SansSerif Bold).
    // See: https://tex.stackexchange.com/questions/22350/difference-between-textrm-and-mathrm
    font: MathFont;
    fontFamily: TextFont;
    fontWeight: FontWeight;
    fontShape: FontShape;
    sizeMultiplier: number;
    maxSize: number;
    minRuleThickness: number;
    _fontMetrics: FontMetrics | undefined;

    /**
     * The base size index.
     */
    static BASESIZE: number = 6;

    constructor(data: OptionsData) {
        this.style = data.style;
        this.color = data.color;
        this.size = data.size || Options.BASESIZE;
        this.textSize = data.textSize || this.size;
        this.phantom = !!data.phantom;
        this.font = data.font || "";
        this.fontFamily = data.fontFamily || "";
        this.fontWeight = data.fontWeight || "";
        this.fontShape = data.fontShape || "";
        this.sizeMultiplier = sizeMultipliers[this.size - 1];
        this.maxSize = data.maxSize;
        this.minRuleThickness = data.minRuleThickness;
        this._fontMetrics = undefined;
    }

    /**
     * Returns a new options object with the same properties as "this".  Properties
     * from "extension" will be copied to the new options object.
     */
    extend(extension: Partial<OptionsData>): Options {
        const data: OptionsData = {
            style: this.style,
            size: this.size,
            textSize: this.textSize,
            color: this.color,
            phantom: this.phantom,
            font: this.font,
            fontFamily: this.fontFamily,
            fontWeight: this.fontWeight,
            fontShape: this.fontShape,
            maxSize: this.maxSize,
            minRuleThickness: this.minRuleThickness,
        };

        Object.assign(data, extension);

        return new Options(data);
    }

    /**
     * Return an options object with the given style. If `this.style === style`,
     * returns `this`.
     */
    havingStyle(style: StyleInterface): Options {
        if (this.style === style) {
            return this;
        } else {
            return this.extend({
                style: style,
                size: sizeAtStyle(this.textSize, style),
            });
        }
    }

    /**
     * Return an options object with a cramped version of the current style. If
     * the current style is cramped, returns `this`.
     */
    havingCrampedStyle(): Options {
        return this.havingStyle(this.style.cramp());
    }

    /**
     * Return an options object with the given size and in at least `\textstyle`.
     * Returns `this` if appropriate.
     */
    havingSize(size: number): Options {
        if (this.size === size && this.textSize === size) {
            return this;
        } else {
            return this.extend({
                style: this.style.text(),
                size: size,
                textSize: size,
                sizeMultiplier: sizeMultipliers[size - 1],
            });
        }
    }

    /**
     * Like `this.havingSize(BASESIZE).havingStyle(style)`. If `style` is omitted,
     * changes to at least `\textstyle`.
     */
    havingBaseStyle(style: StyleInterface): Options {
        style = style || this.style.text();
        const wantSize = sizeAtStyle(Options.BASESIZE, style);
        if (
            this.size === wantSize &&
            this.textSize === Options.BASESIZE &&
            this.style === style
        ) {
            return this;
        } else {
            return this.extend({
                style: style,
                size: wantSize,
            });
        }
    }

    /**
     * Remove the effect of sizing changes such as \Huge.
     * Keep the effect of the current style, such as \scriptstyle.
     */
    havingBaseSizing(): Options {
        let size;
        switch (this.style.id) {
            case 4:
            case 5:
                size = 3; // normalsize in scriptstyle
                break;
            case 6:
            case 7:
                size = 1; // normalsize in scriptscriptstyle
                break;
            default:
                size = 6; // normalsize in textstyle or displaystyle
        }
        return this.extend({
            style: this.style.text(),
            size: size,
        });
    }

    /**
     * Create a new options object with the given color.
     */
    withColor(color: string): Options {
        return this.extend({
            color: color,
        });
    }

    /**
     * Create a new options object with "phantom" set to true.
     */
    withPhantom(): Options {
        return this.extend({
            phantom: true,
        });
    }

    /**
     * Creates a new options object with the given math font or old text font.
     * @type {[type]}
     */
    withFont(font: MathFont): Options {
        return this.extend({font});
    }

    /**
     * Create a new options objects with the given fontFamily.
     */
    withTextFontFamily(fontFamily: TextFont): Options {
        return this.extend({fontFamily, font: ""});
    }

    /**
     * Creates a new options object with the given font weight
     */
    withTextFontWeight(fontWeight: FontWeight): Options {
        return this.extend({
            fontWeight,
            font: "",
        });
    }

    /**
     * Creates a new options object with the given font weight
     */
    withTextFontShape(fontShape: FontShape): Options {
        return this.extend({
            fontShape,
            font: "",
        });
    }

    /**
     * Return the CSS sizing classes required to switch from enclosing options
     * `oldOptions` to `this`. Returns an array of classes.
     */
    sizingClasses(oldOptions: Options): Array<string> {
        if (oldOptions.size !== this.size) {
            return [
                "sizing",
                "reset-size" + oldOptions.size,
                "size" + this.size,
            ];
        } else {
            return [];
        }
    }

    /**
     * Return the CSS sizing classes required to switch to the base size. Like
     * `this.havingSize(BASESIZE).sizingClasses(this)`.
     */
    baseSizingClasses(): Array<string> {
        if (this.size !== Options.BASESIZE) {
            return [
                "sizing",
                "reset-size" + this.size,
                "size" + Options.BASESIZE,
            ];
        } else {
            return [];
        }
    }

    /**
     * Return the font metrics for this size.
     */
    fontMetrics(): FontMetrics {
        if (!this._fontMetrics) {
            this._fontMetrics = getGlobalMetrics(this.size);
        }
        return this._fontMetrics;
    }

    /**
     * Gets the CSS color of the current options object
     */
    getColor(): string | undefined {
        if (this.phantom) {
            return "transparent";
        } else {
            return this.color;
        }
    }
}

export default Options;
