// This helper function for makeVList makes it easier to enforce type safety by
// allowing early exits (returns) in the logic.
const getVListChildrenAndDepth = function(params: VListParam): {
    children: (VListChild | VListElemAndShift)[] | VListChild[];
    depth: number;
} {
    if (params.positionType === "individualShift") {
        const oldChildren = params.children;
        const children: (VListChild | VListElemAndShift)[] = [oldChildren[0]];

        // Add in kerns to the list of params.children to get each element to be
        // shifted to the correct specified shift
        const depth = -oldChildren[0].shift - oldChildren[0].elem.depth;
        let currPos = depth;
        for (let i = 1; i < oldChildren.length; i++) {
            const diff = -oldChildren[i].shift - currPos -
                oldChildren[i].elem.depth;
            const size = diff -
                (oldChildren[i - 1].elem.height +
                 oldChildren[i - 1].elem.depth);

            currPos = currPos + diff;
            children.push({type: "kern", size});
            children.push(oldChildren[i]);
        }

        return {children, depth};
    }

    let depth;
    if (params.positionType === "top") {
        // We always start at the bottom, so calculate the bottom by adding up
        // all the sizes
        let bottom = params.positionData;
        for (let i = 0; i < params.children.length; i++) {
            const child = params.children[i];
            bottom -= child.type === "kern"
                ? child.size
                : child.elem.height + child.elem.depth;
        }
        depth = bottom;
    } else if (params.positionType === "bottom") {
        depth = -params.positionData;
    } else {
        const firstChild = params.children[0];
        if (firstChild.type !== "elem") {
            throw new Error('First child must have type "elem".');
        }
        if (params.positionType === "shift") {
            depth = -firstChild.elem.depth - params.positionData;
        } else if (params.positionType === "firstBaseline") {
            depth = -firstChild.elem.depth;
        } else {
            throw new Error(`Invalid positionType ${params.positionType}.`);
        }
    }

    return {children: params.children, depth};
};

/**
 * Makes a vertical list by stacking elements and kerns on top of each other.
 * Allows for many different ways of specifying the positioning method.
 *
 * See VListParam documentation above.
 */
export const makeVList = function(params: VListParam, options: Options): DomSpan {
    const {children, depth} = getVListChildrenAndDepth(params);

    // Create a strut that is taller than any list item. The strut is added to
    // each item, where it will determine the item's baseline. Since it has
    // `overflow:hidden`, the strut's top edge will sit on the item's line box's
    // top edge and the strut's bottom edge will sit on the item's baseline,
    // with no additional line-height spacing. This allows the item baseline to
    // be positioned precisely without worrying about font ascent and
    // line-height.
    let pstrutSize = 0;
    for (let i = 0; i < children.length; i++) {
        const child = children[i];
        if (child.type === "elem") {
            const elem = child.elem;
            pstrutSize = Math.max(pstrutSize, elem.maxFontSize, elem.height);
        }
    }
    pstrutSize += 2;
    const pstrut = makeSpan(["pstrut"], []);
    pstrut.style.height = makeEm(pstrutSize);

    // Create a new list of actual children at the correct offsets
    const realChildren = [];
    let minPos = depth;
    let maxPos = depth;
    let currPos = depth;
    for (let i = 0; i < children.length; i++) {
        const child = children[i];
        if (child.type === "kern") {
            currPos += child.size;
        } else {
            const elem = child.elem;
            const classes = child.wrapperClasses || [];
            const style = child.wrapperStyle || {};

            const childWrap = makeSpan(classes, [pstrut, elem], undefined, style);
            childWrap.style.top = makeEm(-pstrutSize - currPos - elem.depth);
            if (child.marginLeft) {
                childWrap.style.marginLeft = child.marginLeft;
            }
            if (child.marginRight) {
                childWrap.style.marginRight = child.marginRight;
            }

            realChildren.push(childWrap);
            currPos += elem.height + elem.depth;
        }
        minPos = Math.min(minPos, currPos);
        maxPos = Math.max(maxPos, currPos);
    }

    // The vlist contents go in a table-cell with `vertical-align:bottom`.
    // This cell's bottom edge will determine the containing table's baseline
    // without overly expanding the containing line-box.
    const vlist = makeSpan(["vlist"], realChildren);
    vlist.style.height = makeEm(maxPos);

    // A second row is used if necessary to represent the vlist's depth.
    let rows;
    if (minPos < 0) {
        // We will define depth in an empty span with display: table-cell.
        // It should render with the height that we define. But Chrome, in
        // contenteditable mode only, treats that span as if it contains some
        // text content. And that min-height over-rides our desired height.
        // So we put another empty span inside the depth strut span.
        const emptySpan = makeSpan([], []);
        const depthStrut = makeSpan(["vlist"], [emptySpan]);
        depthStrut.style.height = makeEm(-minPos);

        // Safari wants the first row to have inline content; otherwise it
        // puts the bottom of the *second* row on the baseline.
        const topStrut = makeSpan(["vlist-s"], [new SymbolNode("\u200b")]);

        rows = [makeSpan(["vlist-r"], [vlist, topStrut]),
            makeSpan(["vlist-r"], [depthStrut])];
    } else {
        rows = [makeSpan(["vlist-r"], [vlist])];
    }

    const vtable = makeSpan(["vlist-t"], rows);
    if (rows.length === 2) {
        vtable.classes.push("vlist-t2");
    }
    vtable.height = maxPos;
    vtable.depth = -minPos;
    return vtable;
};

// Glue is a concept from TeX which is a flexible space between elements in
// either a vertical or horizontal list. In KaTeX, at least for now, it's
// static space between elements in a horizontal layout.
export const makeGlue = (measurement: Measurement, options: Options): DomSpan => {
    // Make an empty span for the space
    const rule = makeSpan(["mspace"], [], options);
    const size = calculateSize(measurement, options);
