/**
 * RichText is a container that manages complex text label.
 * It will parse text string and create sub displayable elements respectively.
 */
import {
    TextAlign, TextVerticalAlign, ImageLike, Dictionary, MapToType, FontWeight, FontStyle, NullUndefined
} from '../core/types';
import {
    parseRichText, parsePlainText, CalcInnerTextOverflowAreaOut, calcInnerTextOverflowArea,
    tSpanCreateBoundingRect2,
} from './helper/parseText';
import TSpan, { TSpanStyleProps } from './TSpan';
import { retrieve2, each, normalizeCssArray, trim, retrieve3, extend, keys, defaults } from '../core/util';
import { adjustTextX, adjustTextY } from '../contain/text';
import ZRImage from './Image';
import Rect from './shape/Rect';
import BoundingRect from '../core/BoundingRect';
import { MatrixArray } from '../core/matrix';
import Displayable, {
    DisplayableStatePropNames,
    DisplayableProps,
    DEFAULT_COMMON_ANIMATION_PROPS
} from './Displayable';
import { ZRenderType } from '../zrender';
import Animator from '../animation/Animator';
import Transformable from '../core/Transformable';
import { ElementCommonState } from '../Element';
import { GroupLike } from './Group';
import { DEFAULT_FONT, DEFAULT_FONT_SIZE } from '../core/platform';

type TextContentBlock = ReturnType<typeof parseRichText>
type TextLine = TextContentBlock['lines'][0]
type TextToken = TextLine['tokens'][0]

// TODO Default value?
export interface TextStylePropsPart {
    // TODO Text is assigned inside zrender
    text?: string

    fill?: string
    stroke?: string
    strokeNoScale?: boolean

    opacity?: number
    fillOpacity?: number
    strokeOpacity?: number
    /**
     * textStroke may be set as some color as a default
     * value in upper application, where the default value
     * of lineWidth should be 0 to make sure that
     * user can choose to do not use text stroke.
     */
    lineWidth?: number
    lineDash?: false | number[]
    lineDashOffset?: number
    borderDash?: false | number[]
    borderDashOffset?: number

    /**
     * If `fontSize` or `fontFamily` exists, `font` will be reset by
     * `fontSize`, `fontStyle`, `fontWeight`, `fontFamily`.
     * So do not visit it directly in upper application (like echarts),
     * but use `contain/text#makeFont` instead.
     */
    font?: string
    /**
     * The same as font. Use font please.
     * @deprecated
     */
    textFont?: string

    /**
     * It helps merging respectively, rather than parsing an entire font string.
     */
    fontStyle?: FontStyle
    /**
     * It helps merging respectively, rather than parsing an entire font string.
     */
    fontWeight?: FontWeight
    /**
     * It helps merging respectively, rather than parsing an entire font string.
     */
    fontFamily?: string
    /**
     * It helps merging respectively, rather than parsing an entire font string.
     * Should be 12 but not '12px'.
     */
    fontSize?: number | string

    align?: TextAlign
    verticalAlign?: TextVerticalAlign

    /**
     * Line height. Default to be text height of '国'
     */
    lineHeight?: number
    /**
     * Width of text block. Not include padding
     * Used for background, truncate, wrap
     * If string - be 'auto'.
     */
    width?: number | string
    /**
     * Height of text block. Not include padding
     * Used for background, truncate
     */
    height?: number
    /**
     * Reserved for special functionality, like 'hr'.
     */
    tag?: string

    textShadowColor?: string
    textShadowBlur?: number
    textShadowOffsetX?: number
    textShadowOffsetY?: number

    // Shadow, background, border of text box.
    backgroundColor?: string | {
        image: ImageLike | string
    }

    /**
     * Can be `2` or `[2, 4]` or `[2, 3, 4, 5]`
     */
    padding?: number | number[]
    /**
     * Margin of label. Used when layouting the label.
     */
    margin?: number | number[]

    borderColor?: string
    borderWidth?: number
    borderRadius?: number | number[]

    /**
     * Shadow color for background box.
     */
    shadowColor?: string
    /**
     * Shadow blur for background box.
     */
    shadowBlur?: number
    /**
     * Shadow offset x for background box.
     */
    shadowOffsetX?: number
    /**
     * Shadow offset y for background box.
     */
    shadowOffsetY?: number
}
export interface TextStyleProps extends TextStylePropsPart {

    text?: string

    /**
     * The outer rect (including padding) is placed based on x/y.
     * By default 0.
     */
    x?: number
    y?: number

    /**
     * Only support number in the top block.
     */
    width?: number

    /**
     * Text styles for rich text.
     */
    rich?: Dictionary<TextStylePropsPart>

    /**
     * Strategy when calculated text width exceeds textWidth.
     * break: break by word
     * break: will break inside the word
     * truncate: truncate the text and show ellipsis
     * Do nothing if not set
     */
    overflow?: 'break' | 'breakAll' | 'truncate' | 'none'

    /**
     * Strategy when text lines exceeds textHeight.
     * Do nothing if not set
     */
    lineOverflow?: 'truncate'

    /**
     * Epllipsis used if text is truncated
     */
    ellipsis?: string
    /**
     * Placeholder used if text is truncated to empty
     */
    placeholder?: string
    /**
     * Min characters for truncating
     */
    truncateMinChar?: number
}

export interface TextProps extends DisplayableProps {
    style?: TextStyleProps

    zlevel?: number
    z?: number
    z2?: number

    culling?: boolean
    cursor?: string
}

export type TextState = Pick<TextProps, DisplayableStatePropNames> & ElementCommonState

export type DefaultTextStyle = Pick<TextStyleProps, 'fill' | 'stroke' | 'align' | 'verticalAlign'> & {
    autoStroke?: boolean
    // In text local coord.
    // Exist if and only if `ElementTextConfig['autoOverflowArea']: true`
    overflowRect?: BoundingRect | NullUndefined
};

const DEFAULT_RICH_TEXT_COLOR = {
    fill: '#000'
};
const DEFAULT_STROKE_LINE_WIDTH = 2;

const tmpCITOverflowAreaOut = {} as CalcInnerTextOverflowAreaOut;

// const DEFAULT_TEXT_STYLE: TextStyleProps = {
//     x: 0,
//     y: 0,
//     fill: '#000',
//     stroke: null,
//     opacity: 0,
//     fillOpacity:
// }

export const DEFAULT_TEXT_ANIMATION_PROPS: MapToType<TextProps, boolean> = {
    style: defaults<MapToType<TextStyleProps, boolean>, MapToType<TextStyleProps, boolean>>({
        fill: true,
        stroke: true,
        fillOpacity: true,
        strokeOpacity: true,
        lineWidth: true,
        fontSize: true,
        lineHeight: true,
        width: true,
        height: true,
        textShadowColor: true,
        textShadowBlur: true,
        textShadowOffsetX: true,
        textShadowOffsetY: true,
        backgroundColor: true,
        padding: true,  // TODO needs normalize padding before animate
        borderColor: true,
        borderWidth: true,
        borderRadius: true  // TODO needs normalize radius before animate
    }, DEFAULT_COMMON_ANIMATION_PROPS.style)
 };


interface ZRText {
    animate(key?: '', loop?: boolean): Animator<this>
    animate(key: 'style', loop?: boolean): Animator<this['style']>

    getState(stateName: string): TextState
    ensureState(stateName: string): TextState

    states: Dictionary<TextState>
    stateProxy: (stateName: string) => TextState
}

class ZRText extends Displayable<TextProps> implements GroupLike {

    type = 'text'

    style: TextStyleProps

    /**
     * How to handling label overlap
     *
     * hidden:
     */
    overlap: 'hidden' | 'show' | 'blur'

    /**
     * Will use this to calculate transform matrix
     * instead of Element itself if it's give.
     * Not exposed to developers
     */
    innerTransformable: Transformable

    // Be `true` if and only if the result text is modified due to overflow, due to
    // settings on either `overflow` or `lineOverflow`. Based on this the caller can
    // take some action like showing the original text in a particular tip.
    // Only take effect after rendering. So do not visit it before it.
    isTruncated: boolean

    private _children: (ZRImage | Rect | TSpan)[] = []

    private _childCursor: 0

    private _defaultStyle: DefaultTextStyle = DEFAULT_RICH_TEXT_COLOR

    constructor(opts?: TextProps) {
        super();
        this.attr(opts);
    }

    childrenRef() {
        return this._children;
    }

    update() {

        super.update();

        // Update children
        if (this.styleChanged()) {
            this._updateSubTexts();
        }

        for (let i = 0; i < this._children.length; i++) {
            const child = this._children[i];
            // Set common properties.
            child.zlevel = this.zlevel;
            child.z = this.z;
            child.z2 = this.z2;
            child.culling = this.culling;
            child.cursor = this.cursor;
            child.invisible = this.invisible;
        }
    }

    updateTransform() {
        const innerTransformable = this.innerTransformable;
        if (innerTransformable) {
            innerTransformable.updateTransform();
            if (innerTransformable.transform) {
                this.transform = innerTransformable.transform;
            }
        }
        else {
            super.updateTransform();
        }
    }

    getLocalTransform(m?: MatrixArray): MatrixArray {
        const innerTransformable = this.innerTransformable;
        return innerTransformable
            ? innerTransformable.getLocalTransform(m)
            : super.getLocalTransform(m);
    }

    // TODO override setLocalTransform?
    getComputedTransform() {
        if (this.__hostTarget) {
            // Update host target transform
            this.__hostTarget.getComputedTransform();
            // Update text position.
            this.__hostTarget.updateInnerText(true);
        }

        return super.getComputedTransform();
    }

    private _updateSubTexts() {
        // Reset child visit cursor
        this._childCursor = 0;

        normalizeTextStyle(this.style);
        this.style.rich
            ? this._updateRichTexts()
            : this._updatePlainTexts();

        this._children.length = this._childCursor;

        this.styleUpdated();
    }

    addSelfToZr(zr: ZRenderType) {
        super.addSelfToZr(zr);
        for (let i = 0; i < this._children.length; i++) {
            // Also need mount __zr for case like hover detection.
            // The case: hover on a label (position: 'top') causes host el
            // scaled and label Y position lifts a bit so that out of the
            // pointer, then mouse move should be able to trigger "mouseout".
            this._children[i].__zr = zr;
        }
    }

    removeSelfFromZr(zr: ZRenderType) {
        super.removeSelfFromZr(zr);
        for (let i = 0; i < this._children.length; i++) {
            this._children[i].__zr = null;
        }
    }

    getBoundingRect(): BoundingRect {
        if (this.styleChanged()) {
            this._updateSubTexts();
        }
        if (!this._rect) {
            // TODO: Optimize when using width and overflow: wrap/truncate
            const tmpRect = new BoundingRect(0, 0, 0, 0);
            const children = this._children;
            const tmpMat: MatrixArray = [];
            let rect = null;

            for (let i = 0; i < children.length; i++) {
                const child = children[i];
                const childRect = child.getBoundingRect();
                const transform = child.getLocalTransform(tmpMat);

                if (transform) {
                    tmpRect.copy(childRect);
                    tmpRect.applyTransform(transform);
                    rect = rect || tmpRect.clone();
                    rect.union(tmpRect);
                }
                else {
                    rect = rect || childRect.clone();
                    rect.union(childRect);
                }
            }
            this._rect = rect || tmpRect;
        }
        return this._rect;
    }

    // Can be set in Element. To calculate text fill automatically when textContent is inside element
    setDefaultTextStyle(defaultTextStyle: DefaultTextStyle) {
        // Use builtin if defaultTextStyle is not given.
        this._defaultStyle = defaultTextStyle || DEFAULT_RICH_TEXT_COLOR;
    }

    setTextContent(textContent: never) {
        if (process.env.NODE_ENV !== 'production') {
            throw new Error('Can\'t attach text on another text');
        }
    }

    // getDefaultStyleValue<T extends keyof TextStyleProps>(key: T): TextStyleProps[T] {
    //     // Default value is on the prototype.
    //     return this.style.prototype[key];
    // }

    protected _mergeStyle(targetStyle: TextStyleProps, sourceStyle: TextStyleProps) {
        if (!sourceStyle) {
            return targetStyle;
        }

        // DO deep merge on rich configurations.
        const sourceRich = sourceStyle.rich;
        const targetRich = targetStyle.rich || (sourceRich && {});  // Create a new one if source have rich but target don't

        extend(targetStyle, sourceStyle);

        if (sourceRich && targetRich) {
            // merge rich and assign rich again.
            this._mergeRich(targetRich, sourceRich);
            targetStyle.rich = targetRich;
        }
        else if (targetRich) {
            // If source rich not exists. DON'T override the target rich
            targetStyle.rich = targetRich;
        }

        return targetStyle;
    }

    private _mergeRich(targetRich: TextStyleProps['rich'], sourceRich: TextStyleProps['rich']) {
        const richNames = keys(sourceRich);
        // Merge by rich names.
        for (let i = 0; i < richNames.length; i++) {
            const richName = richNames[i];
            targetRich[richName] = targetRich[richName] || {};
            extend(targetRich[richName], sourceRich[richName]);
        }
    }

    getAnimationStyleProps() {
        return DEFAULT_TEXT_ANIMATION_PROPS;
    }


    private _getOrCreateChild(Ctor: {new(): TSpan}): TSpan
    private _getOrCreateChild(Ctor: {new(): ZRImage}): ZRImage
    private _getOrCreateChild(Ctor: {new(): Rect}): Rect
    private _getOrCreateChild(Ctor: {new(): TSpan | Rect | ZRImage}): TSpan | Rect | ZRImage {
        let child = this._children[this._childCursor];
        if (!child || !(child instanceof Ctor)) {
            child = new Ctor();
        }
        this._children[this._childCursor++] = child;
        child.__zr = this.__zr;
        // TODO to users parent can only be group.
        child.parent = this as any;
        return child;
    }

    private _updatePlainTexts() {
        const style = this.style;
        const textFont = style.font || DEFAULT_FONT;
        const textPadding = style.padding as number[];

        const defaultStyle = this._defaultStyle;
        let baseX = style.x || 0;
        let baseY = style.y || 0;
        const textAlign = style.align || defaultStyle.align || 'left';
        const verticalAlign = style.verticalAlign || defaultStyle.verticalAlign || 'top';

        calcInnerTextOverflowArea(
            tmpCITOverflowAreaOut, defaultStyle.overflowRect, baseX, baseY, textAlign, verticalAlign
        );
        baseX = tmpCITOverflowAreaOut.baseX;
        baseY = tmpCITOverflowAreaOut.baseY;

        const text = getStyleText(style);
        const contentBlock = parsePlainText(
            text,
            style,
            tmpCITOverflowAreaOut.outerWidth,
            tmpCITOverflowAreaOut.outerHeight
        );
        const needDrawBg = needDrawBackground(style);
        const bgColorDrawn = !!(style.backgroundColor);

        const outerHeight = contentBlock.outerHeight;
        const outerWidth = contentBlock.outerWidth;

        const textLines = contentBlock.lines;
        const lineHeight = contentBlock.lineHeight;

        this.isTruncated = !!contentBlock.isTruncated;

        let textX = baseX;
        let textY = adjustTextY(baseY, contentBlock.contentHeight, verticalAlign);

        if (needDrawBg || textPadding) {
            // Consider performance, do not call getTextWidth util necessary.
            const boxX = adjustTextX(baseX, outerWidth, textAlign);
            const boxY = adjustTextY(baseY, outerHeight, verticalAlign);
            needDrawBg && this._renderBackground(style, style, boxX, boxY, outerWidth, outerHeight);
        }
        // PENDING:
        //  Should text bounding rect contains style.padding, style.width, style.height when NO background
        //  and border displayed? It depends on how to define "boundingRect". HTML `getBoundingClientRect`
        //  contains padding in that case. But currently ZRText does not.
        //  If implement that, an extra invisible Rect may need to be added as the placeholder for the bounding
        //  rect computation, considering animation of padding. But will it degrade performance for the most
        //  used plain texts cases?

        // `textBaseline` is set as 'middle'.
        textY += lineHeight / 2;

        if (textPadding) {
            textX = getTextXForPadding(baseX, textAlign, textPadding);
            if (verticalAlign === 'top') {
                textY += textPadding[0];
            }
            else if (verticalAlign === 'bottom') {
                textY -= textPadding[2];
            }
        }

        let defaultLineWidth = 0;
        let usingDefaultStroke = false;
        let useDefaultFill = false;
        const textFill = getFill(
            'fill' in style
                ? style.fill
                : (useDefaultFill = true, defaultStyle.fill)
        );
        const textStroke = getStroke(
            'stroke' in style
                ? style.stroke
                : (!bgColorDrawn
                    // If we use "auto lineWidth" widely, it probably bring about some bad case.
                    // So the current strategy is:
                    // If `style.fill` is specified (i.e., `useDefaultFill` is `false`)
                    // (A) And if `textConfig.insideStroke/outsideStroke` is not specified as a color
                    //   (i.e., `defaultStyle.autoStroke` is `true`), we do not actually display
                    //   the auto stroke because we can not make sure wether the stoke is approperiate to
                    //   the given `fill`.
                    // (B) But if `textConfig.insideStroke/outsideStroke` is specified as a color,
                    // we give the auto lineWidth to display the given stoke color.
                    && (!defaultStyle.autoStroke || useDefaultFill)
                )
                ? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, usingDefaultStroke = true, defaultStyle.stroke)
                : null
        );

        const hasShadow = style.textShadowBlur > 0;

        for (let i = 0; i < textLines.length; i++) {
            const el = this._getOrCreateChild(TSpan);
            // Always create new style.
            const subElStyle: TSpanStyleProps = el.createStyle();
            el.useStyle(subElStyle);
            subElStyle.text = textLines[i];
            subElStyle.x = textX;
            subElStyle.y = textY;
            // Always set textAlign and textBase line, because it is difficute to calculate
            // textAlign from prevEl, and we dont sure whether textAlign will be reset if
            // font set happened.
            if (textAlign) {
                subElStyle.textAlign = textAlign;
            }
            // Force baseline to be "middle". Otherwise, if using "top", the
            // text will offset downward a little bit in font "Microsoft YaHei".
            subElStyle.textBaseline = 'middle';
            subElStyle.opacity = style.opacity;
            // Fill after stroke so the outline will not cover the main part.
            subElStyle.strokeFirst = true;

            if (hasShadow) {
                subElStyle.shadowBlur = style.textShadowBlur || 0;
                subElStyle.shadowColor = style.textShadowColor || 'transparent';
                subElStyle.shadowOffsetX = style.textShadowOffsetX || 0;
                subElStyle.shadowOffsetY = style.textShadowOffsetY || 0;
            }

            // Always override default fill and stroke value.
            subElStyle.stroke = textStroke as string;
            subElStyle.fill = textFill as string;

            if (textStroke) {
                subElStyle.lineWidth = style.lineWidth || defaultLineWidth;
                subElStyle.lineDash = style.lineDash;
                subElStyle.lineDashOffset = style.lineDashOffset || 0;
            }

            subElStyle.font = textFont;
            setSeparateFont(subElStyle, style);

            textY += lineHeight;

            // Always set tspan bounding rect to guarantee the consistency if users lays out based
            // on these bounding rects.
            el.setBoundingRect(tSpanCreateBoundingRect2(
                subElStyle,
                contentBlock.contentWidth,
                contentBlock.calculatedLineHeight,
                // Should text bounding rect includes text stroke width?
                // Pros:
                //   - Intuitively, and by convention, bounding rect of `Path` always includes stroke width.
                // Cons:
                //   - It's unpredictable for users whether "auto stroke" is applied. If stroke width is included
                //     and multiple texts are laid out based on its bounding rect, the position of texts may vary
                //     and is unpredictable - especially in limited space (e.g., see echarts pie label cases).
                //   - "auto stroke" attempts to use the same color as the background to make the border to be
                //     invisible in most cases, thus it might be more reasonable to be excluded from bounding rect.
                // Conclusion:
                //   - If users specifies style.stroke, it will be included into the bounding rect as normal.
                //     Otherwise, keep the stroke width as `0` in this case to guarantee consistency of bounding
                //     rect based layout, regardless of whether "auto stroke" is applied.
                usingDefaultStroke ? 0 : null
            ));
        }
    }


    private _updateRichTexts() {
        const style = this.style;
        const defaultStyle = this._defaultStyle;

        const textAlign = style.align || defaultStyle.align;
        const verticalAlign = style.verticalAlign || defaultStyle.verticalAlign;
        let baseX = style.x || 0;
        let baseY = style.y || 0;

        calcInnerTextOverflowArea(
            tmpCITOverflowAreaOut, defaultStyle.overflowRect, baseX, baseY, textAlign, verticalAlign
        );
        baseX = tmpCITOverflowAreaOut.baseX;
        baseY = tmpCITOverflowAreaOut.baseY;

        // TODO Only parse when text changed?
        const text = getStyleText(style);
        const contentBlock = parseRichText(
            text,
            style,
            tmpCITOverflowAreaOut.outerWidth,
            tmpCITOverflowAreaOut.outerHeight,
            textAlign
        );

        const contentWidth = contentBlock.width;
        const outerWidth = contentBlock.outerWidth;
        const outerHeight = contentBlock.outerHeight;
        const textPadding = style.padding as number[];

        this.isTruncated = !!contentBlock.isTruncated;

        const boxX = adjustTextX(baseX, outerWidth, textAlign);
        const boxY = adjustTextY(baseY, outerHeight, verticalAlign);
        let xLeft = boxX;
        let lineTop = boxY;

        if (textPadding) {
            xLeft += textPadding[3];
            lineTop += textPadding[0];
        }

        let xRight = xLeft + contentWidth;

        if (needDrawBackground(style)) {
            this._renderBackground(style, style, boxX, boxY, outerWidth, outerHeight);
        }
        const bgColorDrawn = !!(style.backgroundColor);

        for (let i = 0; i < contentBlock.lines.length; i++) {
            const line = contentBlock.lines[i];
            const tokens = line.tokens;
            const tokenCount = tokens.length;
            const lineHeight = line.lineHeight;

            let remainedWidth = line.width;
            let leftIndex = 0;
            let lineXLeft = xLeft;
            let lineXRight = xRight;
            let rightIndex = tokenCount - 1;
            let token;

            while (
                leftIndex < tokenCount
                && (token = tokens[leftIndex], !token.align || token.align === 'left')
            ) {
                this._placeToken(token, style, lineHeight, lineTop, lineXLeft, 'left', bgColorDrawn);
                remainedWidth -= token.width;
                lineXLeft += token.width;
                leftIndex++;
            }

            while (
                rightIndex >= 0
                && (token = tokens[rightIndex], token.align === 'right')
            ) {
                this._placeToken(token, style, lineHeight, lineTop, lineXRight, 'right', bgColorDrawn);
                remainedWidth -= token.width;
                lineXRight -= token.width;
                rightIndex--;
            }

            // The other tokens are placed as textAlign 'center' if there is enough space.
            lineXLeft += (contentWidth - (lineXLeft - xLeft) - (xRight - lineXRight) - remainedWidth) / 2;
            while (leftIndex <= rightIndex) {
                token = tokens[leftIndex];
                // Consider width specified by user, use 'center' rather than 'left'.
                this._placeToken(
                    token, style, lineHeight, lineTop,
                    lineXLeft + token.width / 2, 'center', bgColorDrawn
                );
                lineXLeft += token.width;
                leftIndex++;
            }

            lineTop += lineHeight;
        }
    }

    private _placeToken(
        token: TextToken,
        style: TextStyleProps,
        lineHeight: number,
        lineTop: number,
        x: number,
        textAlign: string,
        parentBgColorDrawn: boolean
    ) {
        const tokenStyle = style.rich[token.styleName] || {};
        tokenStyle.text = token.text;

        // 'ctx.textBaseline' is always set as 'middle', for sake of
        // the bias of "Microsoft YaHei".
        const verticalAlign = token.verticalAlign;
        let y = lineTop + lineHeight / 2;
        if (verticalAlign === 'top') {
            y = lineTop + token.height / 2;
        }
        else if (verticalAlign === 'bottom') {
            y = lineTop + lineHeight - token.height / 2;
        }

        const needDrawBg = !token.isLineHolder && needDrawBackground(tokenStyle);
        needDrawBg && this._renderBackground(
            tokenStyle,
            style,
            textAlign === 'right'
                ? x - token.width
                : textAlign === 'center'
                ? x - token.width / 2
                : x,
            y - token.height / 2,
            token.width,
            token.height
        );
        const bgColorDrawn = !!tokenStyle.backgroundColor;

        const textPadding = token.textPadding;
        if (textPadding) {
            x = getTextXForPadding(x, textAlign, textPadding);
            y -= token.height / 2 - textPadding[0] - token.innerHeight / 2;
        }

        const el = this._getOrCreateChild(TSpan);
        const subElStyle: TSpanStyleProps = el.createStyle();
        // Always create new style.
        el.useStyle(subElStyle);

        const defaultStyle = this._defaultStyle;
        let useDefaultFill = false;
        let defaultLineWidth = 0;
        let usingDefaultStroke = false;
        const textFill = getFill(
            'fill' in tokenStyle ? tokenStyle.fill
                : 'fill' in style ? style.fill
                : (useDefaultFill = true, defaultStyle.fill)
        );
        const textStroke = getStroke(
            'stroke' in tokenStyle ? tokenStyle.stroke
                : 'stroke' in style ? style.stroke
                : (
                    !bgColorDrawn
                    && !parentBgColorDrawn
                    // See the strategy explained `_updatePlainTexts`.
                    && (!defaultStyle.autoStroke || useDefaultFill)
                ) ? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, usingDefaultStroke = true, defaultStyle.stroke)
                : null
        );

        const hasShadow = tokenStyle.textShadowBlur > 0
            || style.textShadowBlur > 0;

        subElStyle.text = token.text;
        subElStyle.x = x;
        subElStyle.y = y;
        if (hasShadow) {
            subElStyle.shadowBlur = tokenStyle.textShadowBlur || style.textShadowBlur || 0;
            subElStyle.shadowColor = tokenStyle.textShadowColor || style.textShadowColor || 'transparent';
            subElStyle.shadowOffsetX = tokenStyle.textShadowOffsetX || style.textShadowOffsetX || 0;
            subElStyle.shadowOffsetY = tokenStyle.textShadowOffsetY || style.textShadowOffsetY || 0;
        }

        subElStyle.textAlign = textAlign as CanvasTextAlign;
        // Force baseline to be "middle". Otherwise, if using "top", the
        // text will offset downward a little bit in font "Microsoft YaHei".
        subElStyle.textBaseline = 'middle';
        subElStyle.font = token.font || DEFAULT_FONT;
        subElStyle.opacity = retrieve3(tokenStyle.opacity, style.opacity, 1);


        // TODO inherit each item from top style in token style?
        setSeparateFont(subElStyle, tokenStyle);

        if (textStroke) {
            subElStyle.lineWidth = retrieve3(tokenStyle.lineWidth, style.lineWidth, defaultLineWidth);
            subElStyle.lineDash = retrieve2(tokenStyle.lineDash, style.lineDash);
            subElStyle.lineDashOffset = style.lineDashOffset || 0;
            subElStyle.stroke = textStroke;
        }
        if (textFill) {
            subElStyle.fill = textFill;
        }

        // NOTE: Should not call dirtyStyle after setBoundingRect. Or it will be cleared.
        el.setBoundingRect(tSpanCreateBoundingRect2(
            subElStyle,
            token.contentWidth,
            token.contentHeight,
            // See the strategy explained `_updatePlainTexts`.
            usingDefaultStroke ? 0 : null
        ));
    }

    private _renderBackground(
        style: TextStylePropsPart,
        topStyle: TextStylePropsPart,
        x: number,
        y: number,
        width: number,
        height: number
    ) {
        const textBackgroundColor = style.backgroundColor;
        const textBorderWidth = style.borderWidth;
        const textBorderColor = style.borderColor;
        const isImageBg = textBackgroundColor && (textBackgroundColor as {image: ImageLike}).image;
        const isPlainOrGradientBg = textBackgroundColor && !isImageBg;
        const textBorderRadius = style.borderRadius;
        const self = this;

        let rectEl: Rect;
        let imgEl: ZRImage;
        if (isPlainOrGradientBg || style.lineHeight || (textBorderWidth && textBorderColor)) {
            // Background is color
            rectEl = this._getOrCreateChild(Rect);
            rectEl.useStyle(rectEl.createStyle());    // Create an empty style.
            rectEl.style.fill = null;
            const rectShape = rectEl.shape;
            rectShape.x = x;
            rectShape.y = y;
            rectShape.width = width;
            rectShape.height = height;
            rectShape.r = textBorderRadius;
            rectEl.dirtyShape();
        }

        if (isPlainOrGradientBg) {
            const rectStyle = rectEl.style;
            rectStyle.fill = textBackgroundColor as string || null;
            rectStyle.fillOpacity = retrieve2(style.fillOpacity, 1);
        }
        else if (isImageBg) {
            imgEl = this._getOrCreateChild(ZRImage);
            imgEl.onload = function () {
                // Refresh and relayout after image loaded.
                self.dirtyStyle();
            };
            const imgStyle = imgEl.style;
            imgStyle.image = (textBackgroundColor as {image: ImageLike}).image;
            imgStyle.x = x;
            imgStyle.y = y;
            imgStyle.width = width;
            imgStyle.height = height;
        }

        if (textBorderWidth && textBorderColor) {
            const rectStyle = rectEl.style;
            rectStyle.lineWidth = textBorderWidth;
            rectStyle.stroke = textBorderColor;
            rectStyle.strokeOpacity = retrieve2(style.strokeOpacity, 1);
            rectStyle.lineDash = style.borderDash;
            rectStyle.lineDashOffset = style.borderDashOffset || 0;
            rectEl.strokeContainThreshold = 0;

            // Making shadow looks better.
            if (rectEl.hasFill() && rectEl.hasStroke()) {
                rectStyle.strokeFirst = true;
                rectStyle.lineWidth *= 2;
            }
        }

        const commonStyle = (rectEl || imgEl).style;
        commonStyle.shadowBlur = style.shadowBlur || 0;
        commonStyle.shadowColor = style.shadowColor || 'transparent';
        commonStyle.shadowOffsetX = style.shadowOffsetX || 0;
        commonStyle.shadowOffsetY = style.shadowOffsetY || 0;
        commonStyle.opacity = retrieve3(style.opacity, topStyle.opacity, 1);
    }

    static makeFont(style: TextStylePropsPart): string {
        // FIXME in node-canvas fontWeight is before fontStyle
        // Use `fontSize` `fontFamily` to check whether font properties are defined.
        let font = '';
        if (hasSeparateFont(style)) {
            font = [
                style.fontStyle,
                style.fontWeight,
                parseFontSize(style.fontSize),
                // If font properties are defined, `fontFamily` should not be ignored.
                style.fontFamily || 'sans-serif'
            ].join(' ');
        }
        return font && trim(font) || style.textFont || style.font;
    }
}


const VALID_TEXT_ALIGN = {left: true, right: 1, center: 1};
const VALID_TEXT_VERTICAL_ALIGN = {top: 1, bottom: 1, middle: 1};

const FONT_PARTS = ['fontStyle', 'fontWeight', 'fontSize', 'fontFamily'] as const;

export function parseFontSize(fontSize: number | string) {
    if (
        typeof fontSize === 'string'
        && (
            fontSize.indexOf('px') !== -1
            || fontSize.indexOf('rem') !== -1
            || fontSize.indexOf('em') !== -1
        )
    ) {
        return fontSize;
    }
    else if (!isNaN(+fontSize)) {
        return fontSize + 'px';
    }
    else {
        return DEFAULT_FONT_SIZE + 'px';
    }
}

function setSeparateFont(
    targetStyle: TSpanStyleProps,
    sourceStyle: TextStylePropsPart
) {
    for (let i = 0; i < FONT_PARTS.length; i++) {
        const fontProp = FONT_PARTS[i];
        const val = sourceStyle[fontProp];
        if (val != null) {
            (targetStyle as any)[fontProp] = val;
        }
    }
}

export function hasSeparateFont(style: Pick<TextStylePropsPart, 'fontSize' | 'fontFamily' | 'fontWeight'>) {
    return style.fontSize != null || style.fontFamily || style.fontWeight;
}

export function normalizeTextStyle(style: TextStyleProps): TextStyleProps {
    normalizeStyle(style);
    // TODO inherit each item from top style in token style?
    each(style.rich, normalizeStyle);
    return style;
}

function normalizeStyle(style: TextStylePropsPart) {
    if (style) {
        style.font = ZRText.makeFont(style);
        let textAlign = style.align;
        // 'middle' is invalid, convert it to 'center'
        (textAlign as string) === 'middle' && (textAlign = 'center');
        style.align = (
            textAlign == null || VALID_TEXT_ALIGN[textAlign]
        ) ? textAlign : 'left';

        // Compatible with textBaseline.
        let verticalAlign = style.verticalAlign;
        (verticalAlign as string) === 'center' && (verticalAlign = 'middle');
        style.verticalAlign = (
            verticalAlign == null || VALID_TEXT_VERTICAL_ALIGN[verticalAlign]
        ) ? verticalAlign : 'top';

        // TODO Should not change the orignal value.
        const textPadding = style.padding;
        if (textPadding) {
            style.padding = normalizeCssArray(style.padding);
        }
    }
}

/**
 * @param stroke If specified, do not check style.textStroke.
 * @param lineWidth If specified, do not check style.textStroke.
 */
function getStroke(
    stroke?: TextStylePropsPart['stroke'],
    lineWidth?: number
) {
    return (stroke == null || lineWidth <= 0 || stroke === 'transparent' || stroke === 'none')
        ? null
        : ((stroke as any).image || (stroke as any).colorStops)
        ? '#000'
        : stroke;
}

function getFill(
    fill?: TextStylePropsPart['fill']
) {
    return (fill == null || fill === 'none')
        ? null
        // TODO pattern and gradient?
        : ((fill as any).image || (fill as any).colorStops)
        ? '#000'
        : fill;
}

function getTextXForPadding(x: number, textAlign: string, textPadding: number[]): number {
    return textAlign === 'right'
        ? (x - textPadding[1])
        : textAlign === 'center'
        ? (x + textPadding[3] / 2 - textPadding[1] / 2)
        : (x + textPadding[3]);
}

function getStyleText(style: TextStylePropsPart): string {
    // Compat: set number to text is supported.
    // set null/undefined to text is supported.
    let text = style.text;
    text != null && (text += '');
    return text;
}

/**
 * If needs draw background
 * @param style Style of element
 */
function needDrawBackground(style: TextStylePropsPart): boolean {
    return !!(
        style.backgroundColor
        || style.lineHeight
        || (style.borderWidth && style.borderColor)
    );
}

export default ZRText;