"use strict";
/* eslint-disable no-control-regex */
Object.defineProperty(exports, "__esModule", { value: true });
exports.breakText = exports.splitTextByLength = exports.measureText = exports.text = void 0;
const string_1 = require("../string");
const text_1 = require("../text");
const attr_1 = require("./attr");
const vector_1 = require("../vector");
const elem_1 = require("./elem");
function createTextPathNode(attrs, elem) {
    const vel = vector_1.Vector.create(elem);
    const textPath = vector_1.Vector.create('textPath');
    const d = attrs.d;
    if (d && attrs['xlink:href'] === undefined) {
        const path = vector_1.Vector.create('path').attr('d', d).appendTo(vel.defs());
        textPath.attr('xlink:href', `#${path.id}`);
    }
    if (typeof attrs === 'object') {
        textPath.attr(attrs);
    }
    return textPath.node;
}
function annotateTextLine(lineNode, lineAnnotations, options) {
    const eol = options.eol;
    const baseSize = options.baseSize;
    const lineHeight = options.lineHeight;
    let maxFontSize = 0;
    let tspanNode;
    const fontMetrics = {};
    const lastJ = lineAnnotations.length - 1;
    for (let j = 0; j <= lastJ; j += 1) {
        let annotation = lineAnnotations[j];
        let fontSize = null;
        if (typeof annotation === 'object') {
            const annotationAttrs = annotation.attrs;
            const vTSpan = vector_1.Vector.create('tspan', annotationAttrs);
            tspanNode = vTSpan.node;
            let t = annotation.t;
            if (eol && j === lastJ) {
                t += eol;
            }
            tspanNode.textContent = t;
            // Per annotation className
            const annotationClass = annotationAttrs.class;
            if (annotationClass) {
                vTSpan.addClass(annotationClass);
            }
            // set the list of indices of all the applied annotations
            // in the `annotations` attribute. This list is a comma
            // separated list of indices.
            if (options.includeAnnotationIndices) {
                vTSpan.attr('annotations', annotation.annotations.join(','));
            }
            // Check for max font size
            fontSize = parseFloat(annotationAttrs['font-size']);
            if (fontSize === undefined)
                fontSize = baseSize;
            if (fontSize && fontSize > maxFontSize)
                maxFontSize = fontSize;
        }
        else {
            if (eol && j === lastJ) {
                annotation += eol;
            }
            tspanNode = document.createTextNode(annotation || ' ');
            if (baseSize && baseSize > maxFontSize) {
                maxFontSize = baseSize;
            }
        }
        lineNode.appendChild(tspanNode);
    }
    if (maxFontSize) {
        fontMetrics.maxFontSize = maxFontSize;
    }
    if (lineHeight) {
        fontMetrics.lineHeight = lineHeight;
    }
    else if (maxFontSize) {
        fontMetrics.lineHeight = maxFontSize * 1.2;
    }
    return fontMetrics;
}
const emRegex = /em$/;
function emToPx(em, fontSize) {
    const numerical = parseFloat(em);
    if (emRegex.test(em)) {
        return numerical * fontSize;
    }
    return numerical;
}
function calculateDY(alignment, linesMetrics, baseSizePx, lineHeight) {
    if (!Array.isArray(linesMetrics)) {
        return 0;
    }
    const n = linesMetrics.length;
    if (!n)
        return 0;
    let lineMetrics = linesMetrics[0];
    const flMaxFont = emToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx;
    let rLineHeights = 0;
    const lineHeightPx = emToPx(lineHeight, baseSizePx);
    for (let i = 1; i < n; i += 1) {
        lineMetrics = linesMetrics[i];
        const iLineHeight = emToPx(lineMetrics.lineHeight, baseSizePx) || lineHeightPx;
        rLineHeights += iLineHeight;
    }
    const llMaxFont = emToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx;
    let dy;
    switch (alignment) {
        case 'middle':
            dy = flMaxFont / 2 - 0.15 * llMaxFont - rLineHeights / 2;
            break;
        case 'bottom':
            dy = -(0.25 * llMaxFont) - rLineHeights;
            break;
        case 'top':
        default:
            dy = 0.8 * flMaxFont;
            break;
    }
    return dy;
}
function text(elem, content, options = {}) {
    content = text_1.Text.sanitize(content); // eslint-disable-line
    const eol = options.eol;
    let textPath = options.textPath;
    const verticalAnchor = options.textVerticalAnchor;
    const namedVerticalAnchor = verticalAnchor === 'middle' ||
        verticalAnchor === 'bottom' ||
        verticalAnchor === 'top';
    // Horizontal shift applied to all the lines but the first.
    let x = options.x;
    if (x === undefined) {
        x = elem.getAttribute('x') || 0;
    }
    // Annotations
    const iai = options.includeAnnotationIndices;
    let annotations = options.annotations;
    if (annotations && !Array.isArray(annotations)) {
        annotations = [annotations];
    }
    // Shift all the <tspan> but first by one line (`1em`)
    const defaultLineHeight = options.lineHeight;
    const autoLineHeight = defaultLineHeight === 'auto';
    const lineHeight = autoLineHeight ? '1.5em' : defaultLineHeight || '1em';
    let needEmpty = true;
    const childNodes = elem.childNodes;
    if (childNodes.length === 1) {
        const node = childNodes[0];
        if (node && node.tagName.toUpperCase() === 'TITLE') {
            needEmpty = false;
        }
    }
    if (needEmpty) {
        (0, elem_1.empty)(elem);
    }
    (0, attr_1.attr)(elem, {
        // Preserve spaces, do not consecutive spaces to get collapsed to one.
        'xml:space': 'preserve',
        // An empty text gets rendered into the DOM in webkit-based browsers.
        // In order to unify this behaviour across all browsers
        // we rather hide the text element when it's empty.
        display: content || options.displayEmpty ? null : 'none',
    });
    // Set default font-size if none
    const strFontSize = (0, attr_1.attr)(elem, 'font-size');
    let fontSize = parseFloat(strFontSize);
    if (!fontSize) {
        fontSize = 16;
        if ((namedVerticalAnchor || annotations) && !strFontSize) {
            (0, attr_1.attr)(elem, 'font-size', `${fontSize}`);
        }
    }
    let containerNode;
    if (textPath) {
        // Now all the `<tspan>`s will be inside the `<textPath>`.
        if (typeof textPath === 'string') {
            textPath = { d: textPath };
        }
        containerNode = createTextPathNode(textPath, elem);
    }
    else {
        containerNode = document.createDocumentFragment();
    }
    let dy;
    let offset = 0;
    let annotatedY;
    const lines = content.split('\n');
    const linesMetrics = [];
    const lastI = lines.length - 1;
    for (let i = 0; i <= lastI; i += 1) {
        dy = lineHeight;
        let lineClassName = 'v-line';
        const lineNode = (0, elem_1.createSvgElement)('tspan');
        let lineMetrics;
        let line = lines[i];
        if (line) {
            if (annotations) {
                // Find the *compacted* annotations for this line.
                const lineAnnotations = text_1.Text.annotate(line, annotations, {
                    offset: -offset,
                    includeAnnotationIndices: iai,
                });
                lineMetrics = annotateTextLine(lineNode, lineAnnotations, {
                    eol: i !== lastI && eol,
                    baseSize: fontSize,
                    lineHeight: autoLineHeight ? null : lineHeight,
                    includeAnnotationIndices: iai,
                });
                // Get the line height based on the biggest font size
                // in the annotations for this line.
                const iLineHeight = lineMetrics.lineHeight;
                if (iLineHeight && autoLineHeight && i !== 0) {
                    dy = iLineHeight;
                }
                if (i === 0) {
                    annotatedY = lineMetrics.maxFontSize * 0.8;
                }
            }
            else {
                if (eol && i !== lastI) {
                    line += eol;
                }
                lineNode.textContent = line;
            }
        }
        else {
            // Make sure the textContent is never empty. If it is, add a dummy
            // character and make it invisible, making the following lines correctly
            // relatively positioned. `dy=1em` won't work with empty lines otherwise.
            lineNode.textContent = '-';
            lineClassName += ' v-empty-line';
            const lineNodeStyle = lineNode.style;
            lineNodeStyle.fillOpacity = 0;
            lineNodeStyle.strokeOpacity = 0;
            if (annotations) {
                lineMetrics = {};
            }
        }
        if (lineMetrics) {
            linesMetrics.push(lineMetrics);
        }
        if (i > 0) {
            lineNode.setAttribute('dy', dy);
        }
        // Firefox requires 'x' to be set on the first line
        if (i > 0 || textPath) {
            lineNode.setAttribute('x', x);
        }
        lineNode.className.baseVal = lineClassName;
        containerNode.appendChild(lineNode);
        offset += line.length + 1; // + 1 = newline character.
    }
    // Y Alignment calculation
    if (namedVerticalAnchor) {
        if (annotations) {
            dy = calculateDY(verticalAnchor, linesMetrics, fontSize, lineHeight);
        }
        else if (verticalAnchor === 'top') {
            // A shortcut for top alignment. It does not depend on font-size nor line-height
            dy = '0.8em';
        }
        else {
            let rh; // remaining height
            if (lastI > 0) {
                rh = parseFloat(lineHeight) || 1;
                rh *= lastI;
                if (!emRegex.test(lineHeight))
                    rh /= fontSize;
            }
            else {
                // Single-line text
                rh = 0;
            }
            switch (verticalAnchor) {
                case 'middle':
                    dy = `${0.3 - rh / 2}em`;
                    break;
                case 'bottom':
                    dy = `${-rh - 0.3}em`;
                    break;
                default:
                    break;
            }
        }
    }
    else if (verticalAnchor === 0) {
        dy = '0em';
    }
    else if (verticalAnchor) {
        dy = verticalAnchor;
    }
    else {
        // No vertical anchor is defined
        dy = 0;
        // Backwards compatibility - we change the `y` attribute instead of `dy`.
        if (elem.getAttribute('y') == null) {
            elem.setAttribute('y', `${annotatedY || '0.8em'}`);
        }
    }
    const firstLine = containerNode.firstChild;
    firstLine.setAttribute('dy', dy);
    elem.appendChild(containerNode);
}
exports.text = text;
function measureText(text, styles = {}) {
    const canvasContext = document.createElement('canvas').getContext('2d');
    if (!text) {
        return { width: 0 };
    }
    const font = [];
    const fontSize = styles['font-size']
        ? `${parseFloat(styles['font-size'])}px`
        : '14px';
    font.push(styles['font-style'] || 'normal');
    font.push(styles['font-variant'] || 'normal');
    font.push(styles['font-weight'] || 400);
    font.push(fontSize);
    font.push(styles['font-family'] || 'sans-serif');
    canvasContext.font = font.join(' ');
    return canvasContext.measureText(text);
}
exports.measureText = measureText;
function splitTextByLength(text, splitWidth, totalWidth, style = {}) {
    if (splitWidth >= totalWidth) {
        return [text, ''];
    }
    const length = text.length;
    const caches = {};
    let index = Math.round((splitWidth / totalWidth) * length - 1);
    if (index < 0) {
        index = 0;
    }
    // eslint-disable-next-line
    while (index >= 0 && index < length) {
        const frontText = text.slice(0, index);
        const frontWidth = caches[frontText] || measureText(frontText, style).width;
        const behindText = text.slice(0, index + 1);
        const behindWidth = caches[behindText] || measureText(behindText, style).width;
        caches[frontText] = frontWidth;
        caches[behindText] = behindWidth;
        if (frontWidth > splitWidth) {
            index -= 1;
        }
        else if (behindWidth <= splitWidth) {
            index += 1;
        }
        else {
            break;
        }
    }
    return [text.slice(0, index), text.slice(index)];
}
exports.splitTextByLength = splitTextByLength;
function breakText(text, size, styles = {}, options = {}) {
    const width = size.width;
    const height = size.height;
    const eol = options.eol || '\n';
    const fontSize = styles.fontSize || 14;
    const lineHeight = styles.lineHeight
        ? parseFloat(styles.lineHeight)
        : Math.ceil(fontSize * 1.4);
    const maxLines = Math.floor(height / lineHeight);
    if (text.indexOf(eol) > -1) {
        const delimiter = string_1.StringExt.uuid();
        const splitText = [];
        text.split(eol).map((line) => {
            const part = breakText(line, Object.assign(Object.assign({}, size), { height: Number.MAX_SAFE_INTEGER }), styles, Object.assign(Object.assign({}, options), { eol: delimiter }));
            if (part) {
                splitText.push(...part.split(delimiter));
            }
        });
        return splitText.slice(0, maxLines).join(eol);
    }
    const { width: textWidth } = measureText(text, styles);
    if (textWidth < width) {
        return text;
    }
    const lines = [];
    let remainText = text;
    let remainWidth = textWidth;
    let ellipsis = options.ellipsis;
    let ellipsisWidth = 0;
    if (ellipsis) {
        if (typeof ellipsis !== 'string') {
            ellipsis = '\u2026';
        }
        ellipsisWidth = measureText(ellipsis, styles).width;
    }
    for (let i = 0; i < maxLines; i += 1) {
        if (remainWidth > width) {
            const isLast = i === maxLines - 1;
            if (isLast) {
                const [front] = splitTextByLength(remainText, width - ellipsisWidth, remainWidth, styles);
                lines.push(ellipsis ? `${front}${ellipsis}` : front);
            }
            else {
                const [front, behind] = splitTextByLength(remainText, width, remainWidth, styles);
                lines.push(front);
                remainText = behind;
                remainWidth = measureText(remainText, styles).width;
            }
        }
        else {
            lines.push(remainText);
            break;
        }
    }
    return lines.join(eol);
}
exports.breakText = breakText;
//# sourceMappingURL=text.js.map