import { keys, map } from '../core/util';

export type CSSSelectorVNode = Record<string, string>
export type CSSAnimationVNode = Record<string, Record<string, string>>

export const SVGNS = 'http://www.w3.org/2000/svg';
export const XLINKNS = 'http://www.w3.org/1999/xlink';
export const XMLNS = 'http://www.w3.org/2000/xmlns/';
export const XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace';

export function createElement(name: string) {
    return document.createElementNS(SVGNS, name);
}

export type SVGVNodeAttrs = Record<string, string | number | undefined | boolean>
export interface SVGVNode {
    tag: string,
    attrs: SVGVNodeAttrs,
    children?: SVGVNode[],
    text?: string

    // For patching
    elm?: Node
    key: string
};
export function createVNode(
    tag: string,
    key: string,
    attrs?: SVGVNodeAttrs,
    children?: SVGVNode[],
    text?: string
): SVGVNode {
    return {
        tag,
        attrs: attrs || {},
        children,
        text,
        key
    };
}

function createElementOpen(name: string, attrs?: SVGVNodeAttrs) {
    const attrsStr: string[] = [];
    if (attrs) {
        // eslint-disable-next-line
        for (let key in attrs) {
            const val = attrs[key];
            let part = key;
            // Same with the logic in patch.
            if (val === false) {
                continue;
            }
            else if (val !== true && val != null) {
                part += `="${val}"`;
            }
            attrsStr.push(part);
        }
    }
    return `<${name} ${attrsStr.join(' ')}>`;
}

function createElementClose(name: string) {
    return `</${name}>`;
}

export function vNodeToString(el: SVGVNode, opts?: {
    newline?: boolean
}) {
    opts = opts || {};
    const S = opts.newline ? '\n' : '';
    function convertElToString(el: SVGVNode): string {
        const {children, tag, attrs} = el;
        return createElementOpen(tag, attrs)
            + (el.text || '')
            + (children ? `${S}${map(children, child => convertElToString(child)).join(S)}${S}` : '')
            + createElementClose(tag);
    }
    return convertElToString(el);
}

export function getCssString(
    selectorNodes: Record<string, CSSSelectorVNode>,
    animationNodes: Record<string, CSSAnimationVNode>,
    opts?: {
        newline?: boolean
    }
) {
    opts = opts || {};
    const S = opts.newline ? '\n' : '';
    const bracketBegin = ` {${S}`;
    const bracketEnd = `${S}}`;
    const selectors = map(keys(selectorNodes), className => {
        return className + bracketBegin + map(keys(selectorNodes[className]), attrName => {
            return `${attrName}:${selectorNodes[className][attrName]};`;
        }).join(S) + bracketEnd;
    }).join(S);
    const animations = map(keys(animationNodes), (animationName) => {
        return `@keyframes ${animationName}${bracketBegin}` + map(keys(animationNodes[animationName]), percent => {
            return percent + bracketBegin + map(keys(animationNodes[animationName][percent]), attrName => {
                let val = animationNodes[animationName][percent][attrName];
                // postprocess
                if (attrName === 'd') {
                    val = `path("${val}")`;
                }
                return `${attrName}:${val};`;
            }).join(S) + bracketEnd;
        }).join(S) + bracketEnd;
    }).join(S);

    if (!selectors && !animations) {
        return '';
    }

    return ['<![CDATA[', selectors, animations, ']]>'].join(S);
}


export interface BrushScope {
    zrId: string

    shadowCache: Record<string, string>
    gradientCache: Record<string, string>
    patternCache: Record<string, string>
    clipPathCache: Record<string, string>

    defs: Record<string, SVGVNode>

    cssNodes: Record<string, CSSSelectorVNode>
    cssAnims: Record<string, Record<string, Record<string, string>>>

    cssClassIdx: number
    cssAnimIdx: number

    shadowIdx: number
    gradientIdx: number
    patternIdx: number
    clipPathIdx: number
    // configs
    /**
     * If create animates nodes.
     */
    animation?: boolean,

    /**
     * If will update. Some optimization for string generation can't be applied.
     */
    willUpdate?: boolean

    /**
     * If compress the output string.
     */
    compress?: boolean
}

export function createBrushScope(zrId: string): BrushScope {
    return {
        zrId,
        shadowCache: {},
        patternCache: {},
        gradientCache: {},
        clipPathCache: {},
        defs: {},

        cssNodes: {},
        cssAnims: {},

        cssClassIdx: 0,
        cssAnimIdx: 0,

        shadowIdx: 0,
        gradientIdx: 0,
        patternIdx: 0,
        clipPathIdx: 0
    };
}

export function createSVGVNode(
    width: number | string,
    height: number | string,
    children?: SVGVNode[],
    useViewBox?: boolean
) {
    return createVNode(
        'svg',
        'root',
        {
            'width': width,
            'height': height,
            'xmlns': SVGNS,
            'xmlns:xlink': XLINKNS,
            'version': '1.1',
            'baseProfile': 'full',
            'viewBox': useViewBox ? `0 0 ${width} ${height}` : false
        },
        children
    );
}