import { useEffect, useRef, useState } from 'react';
import JsBarcode from 'jsbarcode';
import { ELEMENT_TYPES } from '../schema/documentSchema';
import {
    buildBoxShadow,
    buildTextShadow,
    resolvePadding,
    resolveBorder,
    resolveBorderRadius,
    resolveFillBackground,
    getCornerRadii,
    resolveFontStack,
    buildTextStroke,
    buildBorderPaint,
    borderRingStyle,
    isGradientText,
    resolveTextFill,
    resolveSpanFill,
    SHAPE_BORDER_KEYS,
} from '../panels/propertyControls';
import { elementColorSpans, paintSegments } from '../utils/textRuns';
import { measureNaturalText, minTextBox } from '../utils/textMetrics';
import { isPathShape } from '../schema/shapeLibrary';
import { paletteRamp } from '../schema/colorPalettes';
import { recolorImage, peekRecoloredImage, rememberRecolored } from '../utils/paletteImage';
import { getCrop, cropImageStyle } from '../utils/imageCrop';
import { renderQrDataUrl } from '../utils/qrCode';
import { isTextOnPath } from '../utils/textPathLayout';
import ShapeVector from './ShapeVector';
import TextOnPath from './TextOnPath';
import TextPathEditor from './TextPathEditor';

/**
 * The QR as an image, logo and all — through the shared renderer, so what the
 * canvas shows is what the export draws (see utils/qrCode).
 */
function useQrDataUrl(element) {
    const [url, setUrl] = useState('');
    const styles = element.styles || {};
    useEffect(() => {
        let cancelled = false;
        renderQrDataUrl(element, { size: 512 })
            .then((u) => { if (!cancelled) setUrl(u); })
            .catch(() => { if (!cancelled) setUrl(''); });
        return () => { cancelled = true; };
        // Re-drawn only for what the code itself is made of — not for a move or
        // a resize, which the <img> handles by itself.
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [
        element.content, element.src,
        styles.foreground, styles.background, styles.margin, styles.errorCorrection,
        styles.logoSize, styles.logoPadding, styles.logoRadius, styles.logoBackground,
    ]);
    return url;
}

function radiusCss(styles) {
    return resolveBorderRadius(styles);
}

function QrPreview({ element }) {
    const styles = element.styles || {};
    const url = useQrDataUrl(element);
    const shadow = buildBoxShadow(styles);
    if (!url) {
        return <div className="w-full h-full bg-zinc-100 flex items-center justify-center text-[10px] text-zinc-400">QR</div>;
    }
    return (
        <img
            src={url}
            alt="QR"
            className="w-full h-full object-contain pointer-events-none"
            draggable={false}
            style={{
                borderRadius: radiusCss(styles),
                border: resolveBorder(styles),
                boxShadow: shadow,
                background: buildBorderPaint(styles, styles.background || '#ffffff'),
            }}
        />
    );
}

function BarcodePreview({ element }) {
    const ref = useRef(null);
    const styles = element.styles || {};
    useEffect(() => {
        if (!ref.current) return;
        try {
            JsBarcode(ref.current, element.content || '0', {
                format: element.format || 'CODE128',
                lineColor: styles.lineColor || '#000000',
                background: styles.background || '#ffffff',
                displayValue: styles.displayValue !== false,
                margin: styles.margin ?? 4,
                height: Math.max(20, (element.height || 70) - 20),
                width: styles.barWidth ?? 1.6,
            });
        } catch {
            // invalid barcode content
        }
    }, [element.content, element.format, element.styles, element.height]);
    return (
        <div
            className="w-full h-full overflow-hidden"
            style={{
                borderRadius: radiusCss(styles),
                border: resolveBorder(styles),
                boxShadow: buildBoxShadow(styles),
                background: buildBorderPaint(styles, styles.background || '#ffffff'),
            }}
        >
            <svg ref={ref} className="w-full h-full" />
        </div>
    );
}

function TablePreview({ element }) {
    const cells = element.cells || [];
    const styles = element.styles || {};
    const bw = styles.borderWidth ?? 1;
    const vAlign = styles.verticalAlign || 'middle';
    return (
        <div
            className="w-full h-full overflow-hidden"
            style={{
                borderRadius: radiusCss(styles),
                boxShadow: buildBoxShadow(styles),
            }}
        >
            <table
                className="w-full h-full border-collapse"
                style={{
                    fontFamily: resolveFontStack(styles),
                    fontSize: styles.fontSize,
                    fontWeight: styles.fontWeight,
                    color: styles.color,
                    textAlign: styles.textAlign || 'left',
                }}
            >
                <tbody>
                    {cells.map((row, ri) => (
                        <tr key={ri}>
                            {(row || []).map((cell, ci) => (
                                <td
                                    key={ci}
                                    style={{
                                        border: bw ? `${bw}px solid ${styles.borderColor || '#d4d4d8'}` : 'none',
                                        padding: styles.cellPadding ?? 6,
                                        background: ri === 0
                                            ? (styles.headerBg || '#f4f4f5')
                                            : (styles.cellBg && styles.cellBg !== 'transparent' ? styles.cellBg : 'transparent'),
                                        fontWeight: ri === 0 ? 600 : (styles.fontWeight || 400),
                                        verticalAlign: vAlign === 'middle' ? 'middle' : vAlign === 'bottom' ? 'bottom' : 'top',
                                    }}
                                >
                                    {cell}
                                </td>
                            ))}
                        </tr>
                    ))}
                </tbody>
            </table>
        </div>
    );
}

function ShapePreview({ element }) {
    const s = element.styles || {};
    // Triangle, polygon, star, arrow… — an outline CSS can't describe, drawn from
    // the shared shape geometry instead (the export replays the same commands).
    if (isPathShape(element.shape)) return <ShapeVector element={element} />;
    const shadow = buildBoxShadow(s);
    const fillBg = resolveFillBackground(s, 'fill');
    const common = {
        width: '100%',
        height: '100%',
        // A shape's "stroke" is its box border; SHAPE_BORDER_KEYS maps the older
        // stroke* names onto the shared border implementation, gradients included.
        background: buildBorderPaint(s, fillBg, SHAPE_BORDER_KEYS),
        border: resolveBorder(s, SHAPE_BORDER_KEYS),
        boxSizing: 'border-box',
        boxShadow: shadow,
    };
    if (element.shape === 'ellipse') {
        return <div style={{ ...common, borderRadius: '50%' }} />;
    }
    if (element.shape === 'line') {
        return (
            <div
                style={{
                    width: '100%',
                    height: '100%',
                    background: s.fillType && s.fillType !== 'solid'
                        ? fillBg
                        : (s.fill || s.stroke || '#a1a1aa'),
                    borderRadius: 2,
                    boxShadow: shadow,
                }}
            />
        );
    }
    return <div style={{ ...common, borderRadius: radiusCss(s) }} />;
}

function verticalJustify(align) {
    if (align === 'middle') return 'center';
    if (align === 'bottom') return 'flex-end';
    return 'flex-start';
}

function textBoxStyle(styles) {
    const bg = resolveFillBackground(styles, 'background');
    const hasFill = styles.backgroundType === 'linear'
        || styles.backgroundType === 'radial'
        || (styles.backgroundColor && styles.backgroundColor !== 'transparent');
    const stroke = buildTextStroke(styles);
    return {
        fontFamily: resolveFontStack(styles),
        fontSize: styles.fontSize,
        fontWeight: styles.fontWeight,
        fontStyle: styles.fontStyle,
        textAlign: styles.textAlign,
        color: styles.color,
        lineHeight: styles.lineHeight,
        letterSpacing: styles.letterSpacing != null ? `${styles.letterSpacing}px` : undefined,
        textDecoration: styles.textDecoration,
        textTransform: styles.textTransform,
        whiteSpace: styles.whiteSpace || 'pre-wrap',
        // A gradient border is painted by its own ring overlay (borderRingStyle),
        // so this is the element's own fill and nothing else.
        background: hasFill ? bg : 'transparent',
        padding: resolvePadding(styles) || (styles.padding != null ? `${styles.padding}px` : undefined),
        border: resolveBorder(styles),
        borderRadius: radiusCss(styles),
        boxShadow: buildBoxShadow(styles),
        textShadow: buildTextShadow(styles),
        // Stroke follows the GLYPH outline, like Figma's stroke on a text node.
        WebkitTextStroke: stroke ? `${stroke.width}px ${stroke.color}` : undefined,
        paintOrder: stroke?.paintOrder,
        boxSizing: 'border-box',
        width: '100%',
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        justifyContent: verticalJustify(styles.verticalAlign || 'top'),
        overflow: 'hidden',
    };
}

/**
 * The image to actually paint: the palette-recoloured bitmap once it is ready, the
 * original until then (and for good, if the pixels can't be read). Recolouring is
 * derived and cached, never stored in the document — see utils/paletteImage.
 */
function usePaletteImage(src, paletteId) {
    const ramp = paletteRamp(paletteId);
    const [recolored, setRecolored] = useState(
        () => (ramp ? peekRecoloredImage(src, paletteId) : null),
    );

    useEffect(() => {
        if (!src || !ramp) {
            setRecolored(null);
            return undefined;
        }
        let dead = false;
        setRecolored(peekRecoloredImage(src, paletteId));
        recolorImage(src, ramp, paletteId).then((url) => {
            rememberRecolored(src, paletteId, url);
            if (!dead) setRecolored(url);
        });
        return () => { dead = true; };
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [src, paletteId]);

    return recolored || src;
}

/**
 * Box chrome for text on a path — background, border and box shadows, without any
 * of the block-text layout. The glyph layer is a SIBLING of this div rather than a
 * child, so the curve is measured against the full element box (a border would
 * otherwise inset it) and letters may overhang the box, as they do in Figma.
 */
function pathBoxStyle(styles) {
    const bg = resolveFillBackground(styles, 'background');
    const hasFill = styles.backgroundType === 'linear'
        || styles.backgroundType === 'radial'
        || (styles.backgroundColor && styles.backgroundColor !== 'transparent');
    return {
        position: 'absolute',
        inset: 0,
        boxSizing: 'border-box',
        background: hasFill ? bg : 'transparent',
        border: resolveBorder(styles),
        borderRadius: radiusCss(styles),
        boxShadow: buildBoxShadow(styles),
    };
}

/** The gradient-border ring for a text box, ready to drop in beside it. */
function borderRingNode(styles) {
    const ring = borderRingStyle(styles);
    if (!ring) return null;
    return <div aria-hidden style={{ ...ring, borderRadius: radiusCss(styles) }} />;
}

/**
 * A gradient glyph fill, CSS-side: the gradient is painted as the background of
 * the text block and then clipped to the letterforms. It goes on the INNER block
 * rather than the element box, because the box's own background and its gradient
 * border already occupy that background — and because clipping to the block is
 * what makes the gradient span the text, matching what the export draws.
 */
function textFillStyle(styles) {
    if (!isGradientText(styles)) return null;
    return {
        backgroundImage: resolveTextFill(styles),
        backgroundSize: '100% 100%',
        WebkitBackgroundClip: 'text',
        backgroundClip: 'text',
        WebkitTextFillColor: 'transparent',
        color: 'transparent',
    };
}

/**
 * One piece of text that paints in its own fill.
 *
 * `fontSize: inherit` is not cosmetic: styles/global.scss sets `span {
 * font-size: 12px }` for the rest of the app, so an uninherited span would
 * shrink the moment a run was coloured. Every piece carries it, coloured or not.
 *
 * `WebkitTextFillColor` is set as well as `color`: a gradient parent sets it to
 * transparent, and it is inherited, so a coloured piece has to say its colour in
 * the same property to win. A gradient run paints the same way the whole element
 * does — background clipped to the glyphs — with `box-decoration-break: clone`
 * so a run that wraps gets the gradient on each line, which is exactly the box
 * the export draws its own gradient across.
 */
function segmentStyle(seg) {
    const base = { fontSize: 'inherit' };
    if (seg.fill) {
        return {
            ...base,
            backgroundImage: resolveSpanFill(seg),
            backgroundSize: '100% 100%',
            WebkitBoxDecorationBreak: 'clone',
            boxDecorationBreak: 'clone',
            WebkitBackgroundClip: 'text',
            backgroundClip: 'text',
            WebkitTextFillColor: 'transparent',
            color: 'transparent',
        };
    }
    if (seg.color) {
        return {
            ...base, color: seg.color, WebkitTextFillColor: seg.color, backgroundImage: 'none',
        };
    }
    return base;
}

/**
 * The element's text, split into its own-filled pieces.
 *
 * Returns the plain string when nothing is individually coloured, so the common
 * case renders exactly the single text node it always did.
 */
function renderTextContent(element, content) {
    const spans = elementColorSpans(element, content);
    if (!spans.length) return content;
    return paintSegments(spans, content).map((seg) => (
        <span
            key={`${seg.start}:${seg.fill ? seg.fill.type : seg.color || 'base'}`}
            style={segmentStyle(seg)}
        >
            {seg.text}
        </span>
    ));
}

/**
 * Editing a text block on canvas.
 *
 * A <textarea> can only be one colour, so the colours would vanish the moment
 * you double-clicked the very text you were colouring. Instead the textarea is
 * made transparent (caret and selection still show) and laid exactly over a
 * mirror of the same text rendered with its per-character colours — the standard
 * highlighter overlay. Both layers inherit the identical font, spacing, padding
 * and wrapping from the element's own box style, so the caret sits on the glyph
 * it appears to sit on.
 *
 * The textarea also reports its selection upward: character offsets are exactly
 * what the colour spans are keyed on, which is why this stayed a textarea rather
 * than becoming a contenteditable with its DOM-range bookkeeping.
 */
function TextBlockEditor({ element, styles, box, onInput, onCommit, onSelect }) {
    const ref = useRef(null);
    const [value, setValue] = useState(element.content || '');

    // Mirrors need the block's own metrics, not the flex box around them.
    const { display, flexDirection, justifyContent, overflow, ...blockBox } = box;

    const report = () => {
        const node = ref.current;
        if (node) onSelect?.({ start: node.selectionStart, end: node.selectionEnd });
    };

    // The mirror is what gives the stack its height, so it must never collapse:
    // a trailing newline leaves no line box of its own (the caret would sit above
    // the text), and empty text would leave nothing to click into at all.
    const mirrored = value === '' ? '\u200b' : (value.endsWith('\n') ? `${value}\n` : value);

    return (
        <div style={{ ...box, position: 'relative' }}>
            <div style={{ width: '100%', position: 'relative' }}>
                <div
                    aria-hidden="true"
                    className="break-words"
                    style={{ ...textFillStyle(styles), width: '100%', pointerEvents: 'none' }}
                >
                    {renderTextContent({ ...element, content: mirrored }, mirrored)}
                </div>
                <textarea
                    ref={ref}
                    autoFocus
                    value={value}
                    className="resize-none outline-none border-none"
                    style={{
                        ...blockBox,
                        position: 'absolute',
                        inset: 0,
                        width: '100%',
                        height: '100%',
                        display: 'block',
                        padding: 0,
                        border: 'none',
                        background: 'transparent',
                        boxShadow: 'none',
                        textShadow: 'none',
                        WebkitTextStroke: undefined,
                        // Transparent glyphs, visible caret: the mirror underneath is
                        // what the user reads, and the browser still paints the
                        // selection highlight over it.
                        color: 'transparent',
                        WebkitTextFillColor: 'transparent',
                        caretColor: styles.color || '#18181b',
                        overflow: 'hidden',
                    }}
                    onChange={(e) => {
                        setValue(e.target.value);
                        onInput?.(e.target.value);
                        report();
                    }}
                    onSelect={report}
                    onKeyUp={report}
                    onBlur={(e) => onCommit?.(e.target.value)}
                    onKeyDown={(e) => {
                        if (e.key === 'Escape') e.currentTarget.blur();
                        e.stopPropagation();
                    }}
                    onPointerDown={(e) => e.stopPropagation()}
                />
            </div>
        </div>
    );
}

/**
 * Renders a single canvas element (absolute box contents).
 */
export default function ElementRenderer({
    element, isEditing, onCommitText, onTextInput, onTextSelect, onAutoSize,
    // Ceiling for a hugging text box, normally the artboard's width. 0 / absent
    // means no ceiling (a preview with no page behind it).
    maxAutoWidth = 0,
    // Text on a path draws its curve as a selection guide — editor chrome only,
    // so the export never sees it.
    showPathGuide = false,
}) {
    const styles = element.styles || {};

    // Palette-recoloured bitmap. Called unconditionally (hook order) and a no-op for
    // every element type that isn't an image or that carries no palette.
    const imageSrc = usePaletteImage(
        element.type === ELEMENT_TYPES.IMAGE ? element.src : null,
        element.paletteId,
    );

    // Box = natural text size + padding + border, measured from an unconstrained probe
    // (so it can't oscillate).
    //
    // ONE rule: re-fit whenever something that decides the text's size changes —
    // content, font, padding, border. Deliberately NOT on mount (opening a template
    // must not silently re-fit and autosave every box) and NOT on width/height changes
    // (so resizing by hand sticks, until the next content/style edit).
    //
    // `textAutoSize: 'height'` is the other half of that rule: a box whose WIDTH is
    // the design (an imported paragraph frame, a column) keeps it and grows down
    // instead. Hugging such a box pulled every wrapped paragraph out into one line
    // thousands of pixels wide on the first keystroke.
    const sigRef = useRef(undefined);
    const autoHeightOnly = element.textAutoSize === 'height';

    useEffect(() => {
        const isTextLike = element.type === ELEMENT_TYPES.TEXT
            || element.type === ELEMENT_TYPES.PLACEHOLDER;
        // On a path the box IS the curve's frame — hugging the text would resize the
        // curve out from under the run on every edit.
        if (!onAutoSize || !isTextLike || isTextOnPath(element)) return;

        const sig = JSON.stringify([
            element.content, element.placeholderKey,
            styles.fontSize, styles.fontFamily, styles.fontWeight, styles.fontStyle,
            styles.lineHeight, styles.letterSpacing, styles.whiteSpace, styles.textTransform,
            styles.padding, styles.paddingTop, styles.paddingBottom,
            styles.paddingLeft, styles.paddingRight, styles.borderWidth,
            // A fixed-width box re-wraps when it is resized, so its width is part of
            // what decides its height. A hugging box ignores its own width by design.
            autoHeightOnly ? element.width : null,
        ]);
        const prev = sigRef.current;
        sigRef.current = sig;
        if (prev === undefined || prev === sig) return;

        if (autoHeightOnly) {
            // Wrapped at the width the box actually has. Grow-only: an author who
            // drew a roomy frame keeps the room when the text gets shorter.
            const min = minTextBox(element, styles, element.width);
            if (min && min.height > element.height + 1) onAutoSize({ height: min.height });
            return;
        }

        const nat = measureNaturalText(element, styles);
        if (!nat) return;
        const p = (side) => Number(styles[`padding${side}`] ?? styles.padding) || 0;
        const bw = Number(styles.borderWidth) || 0;
        let w = Math.max(16, Math.ceil(nat.w + p('Left') + p('Right') + bw * 2));
        let h = Math.max(8, Math.ceil(nat.h + p('Top') + p('Bottom') + bw * 2));
        // Hugging a run longer than the page would push the box off the artboard
        // in one keystroke. Stop at the artboard's width and let it wrap there.
        if (maxAutoWidth > 0 && w > maxAutoWidth) {
            // `minTextBox`'s own width is the widest WORD, not the wrap width — pair
            // it with that and the box would be far narrower than the height it
            // reports. The ceiling IS the wrap width, so take it for both.
            const wrapped = minTextBox(element, styles, maxAutoWidth);
            if (wrapped) {
                w = Math.max(16, maxAutoWidth);
                h = wrapped.height;
            }
        }
        if (Math.abs(w - element.width) > 1 || Math.abs(h - element.height) > 1) {
            onAutoSize({ width: w, height: h });
        }
        // Only re-measure when something that affects text layout changes. Running on
        // every render churned the document (and re-registered the canvas drag
        // listeners) during ordinary interactions like selecting or panning.
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [
        element.content, element.placeholderKey, autoHeightOnly, maxAutoWidth,
        autoHeightOnly ? element.width : null,
        styles.fontSize, styles.fontFamily, styles.fontWeight, styles.fontStyle,
        styles.lineHeight, styles.letterSpacing, styles.whiteSpace, styles.textTransform,
        styles.padding, styles.paddingTop, styles.paddingBottom,
        styles.paddingLeft, styles.paddingRight, styles.borderWidth,
    ]);

    if (!element.visible) return null;

    if (element.type === ELEMENT_TYPES.TEXT || element.type === ELEMENT_TYPES.PLACEHOLDER) {
        const box = textBoxStyle(styles);
        if (isEditing && isTextOnPath(element)) {
            // Edit ON the curve: the run reflows per keystroke instead of dropping to
            // a flat block until blur.
            return (
                <>
                    <div style={pathBoxStyle(styles)} />
                    {borderRingNode(styles)}
                    <TextPathEditor
                        element={element}
                        onInput={onTextInput}
                        onCommit={onCommitText}
                        onSelect={onTextSelect}
                    />
                </>
            );
        }
        if (isEditing) {
            return (
                <>
                    <TextBlockEditor
                        element={element}
                        styles={styles}
                        box={box}
                        onInput={onTextInput}
                        onCommit={onCommitText}
                        onSelect={onTextSelect}
                    />
                    {borderRingNode(styles)}
                </>
            );
        }
        if (isTextOnPath(element)) {
            return (
                <>
                    <div style={pathBoxStyle(styles)} />
                    {borderRingNode(styles)}
                    <TextOnPath element={element} showGuide={showPathGuide} />
                </>
            );
        }
        const shown = element.content
            || (element.type === ELEMENT_TYPES.PLACEHOLDER ? `{{${element.placeholderKey}}}` : '');
        return (
            <>
                <div className="break-words" style={box}>
                    <div style={{ width: '100%', ...textFillStyle(styles) }}>
                        {renderTextContent(element, shown)}
                    </div>
                </div>
                {borderRingNode(styles)}
            </>
        );
    }

    if (element.type === ELEMENT_TYPES.IMAGE) {
        const shadow = buildBoxShadow(styles);
        const bg = resolveFillBackground(styles, 'background');
        const hasFill = styles.backgroundType === 'linear'
            || styles.backgroundType === 'radial'
            || (styles.backgroundColor && styles.backgroundColor !== 'transparent');
        const radii = getCornerRadii(styles);
        // The gradient border rides as its own masked ring — a picture's fill is
        // usually transparent, and the two-layer background trick needs an opaque
        // one to hide behind (see borderRingStyle).
        const ring = borderRingStyle(styles);
        const ringNode = ring
            ? <div aria-hidden style={{ ...ring, borderRadius: radiusCss(styles) }} />
            : null;
        const wrapperStyle = {
            width: '100%',
            height: '100%',
            boxSizing: 'border-box',
            padding: resolvePadding(styles),
            background: hasFill ? bg : 'transparent',
            borderRadius: radiusCss(styles),
            border: resolveBorder(styles),
            boxShadow: shadow,
            overflow: 'hidden',
        };
        if (!element.src) {
            return (
                <>
                    <div
                        className="bg-zinc-100 border border-dashed border-zinc-300 flex items-center justify-center text-xs text-zinc-400"
                        style={wrapperStyle}
                    >
                        Image
                    </div>
                    {ringNode}
                </>
            );
        }
        const imgRadius = Math.max(
            0,
            Math.min(radii.tl, radii.tr, radii.br, radii.bl) - (styles.borderWidth ?? 0),
        );
        // A crop replaces the fit mode entirely: the stored source rect is blown up
        // and offset so it lands exactly on the picture box, clipped by the wrapper.
        const crop = getCrop(element);
        if (crop) {
            return (
                <>
                    <div style={wrapperStyle}>
                        <div
                            className="relative w-full h-full overflow-hidden pointer-events-none"
                            style={{ borderRadius: imgRadius }}
                        >
                            <img
                                src={imageSrc || element.src}
                                alt=""
                                draggable={false}
                                style={cropImageStyle(crop)}
                            />
                        </div>
                    </div>
                    {ringNode}
                </>
            );
        }
        return (
            <>
                <div style={wrapperStyle}>
                    <img
                        src={imageSrc || element.src}
                        alt=""
                        className="w-full h-full pointer-events-none"
                        draggable={false}
                        style={{
                            objectFit: element.fit === 'fit' ? 'contain' : (element.fit || 'contain'),
                            objectPosition: styles.objectPosition || 'center',
                            borderRadius: imgRadius,
                        }}
                    />
                </div>
                {ringNode}
            </>
        );
    }

    if (element.type === ELEMENT_TYPES.SHAPE) return <ShapePreview element={element} />;
    if (element.type === ELEMENT_TYPES.QRCODE) return <QrPreview element={element} />;
    if (element.type === ELEMENT_TYPES.BARCODE) return <BarcodePreview element={element} />;
    if (element.type === ELEMENT_TYPES.TABLE) return <TablePreview element={element} />;

    return null;
}
