/**
 * Live renderer for PATH shapes (triangle, polygon, star, arrow, …) — the ones
 * whose silhouette isn't the element box, so CSS border-radius can't draw them.
 *
 * Layout is a sandwich, bottom to top:
 *   1. an SVG holding one caster path per drop shadow (shadow-only filters)
 *   2. a div carrying the CSS fill, clipped to the outline with clip-path
 *   3. an SVG holding the inner shadows and the stroke
 *
 * Why the fill is a clipped div rather than an SVG <path fill>: it reuses the
 * very same CSS the box shapes use (`resolveFillBackground`), so a gradient fill
 * looks identical whether the element is a rectangle or a star, with one
 * implementation instead of two.
 *
 * The outline itself comes from schema/shapeLibrary — the export replays the same
 * commands onto Canvas2D, which is what keeps the PNG/PDF matching the canvas.
 * The one deliberate gap: an inner shadow's SPREAD is ignored on path shapes
 * (growing an arbitrary outline inward has no exact CSS or canvas equivalent);
 * drop-shadow spread is honoured, by dilating the caster with a stroke.
 */
import { useId } from 'react';
import {
    resolveFillBackground, getEffects, shadowColorCss, borderWidthOf,
    isGradientBorder, borderFillPaint, SHAPE_BORDER_KEYS, GRADIENT_ANGLE_DEFAULT,
} from '../panels/propertyControls';
import { shapePathD, strokeDashArray } from '../schema/shapeLibrary';

/** How far a shadow reaches past the outline — sizes the SVG filter region. */
function shadowPad(fx) {
    return Math.ceil(
        (Number(fx.blur) || 0)
        + Math.abs(Number(fx.spread) || 0)
        + Math.abs(Number(fx.offsetX) || 0)
        + Math.abs(Number(fx.offsetY) || 0)
        + 4,
    );
}

/**
 * SVG paint for a gradient, matching the CSS gradients used everywhere else:
 * 0° points up and grows clockwise, and the gradient line spans the box the way
 * `linear-gradient` does (|w·sinθ| + |h·cosθ|).
 */
function GradientDef({ id, paint, w, h }) {
    const c1 = paint.color1 || paint.color || '#ffffff';
    const c2 = paint.color2 || '#000000';
    if (paint.type === 'radial') {
        const cx = (paint.centerX != null ? Number(paint.centerX) : 50) / 100;
        const cy = (paint.centerY != null ? Number(paint.centerY) : 50) / 100;
        return (
            <radialGradient
                id={id}
                gradientUnits="userSpaceOnUse"
                cx={cx * w}
                cy={cy * h}
                r={Math.hypot(w, h) / 2}
            >
                <stop offset="0" stopColor={c1} />
                <stop offset="1" stopColor={c2} />
            </radialGradient>
        );
    }
    const a = (((Number(paint.angle) || GRADIENT_ANGLE_DEFAULT) - 90) * Math.PI) / 180;
    const dx = Math.cos(a);
    const dy = Math.sin(a);
    const len = Math.abs(w * dx) + Math.abs(h * dy);
    return (
        <linearGradient
            id={id}
            gradientUnits="userSpaceOnUse"
            x1={w / 2 - (dx * len) / 2}
            y1={h / 2 - (dy * len) / 2}
            x2={w / 2 + (dx * len) / 2}
            y2={h / 2 + (dy * len) / 2}
        >
            <stop offset="0" stopColor={c1} />
            <stop offset="1" stopColor={c2} />
        </linearGradient>
    );
}

/**
 * Shadow-only filter: blur + offset the source silhouette, then flood it with the
 * shadow colour. The source is a caster path that is never painted itself, so the
 * filter output IS the shadow. stdDeviation = blur/2 matches both CSS box-shadow
 * and canvas shadowBlur.
 */
function DropShadowFilter({ id, fx, w, h }) {
    const pad = shadowPad(fx);
    return (
        <filter
            id={id}
            filterUnits="userSpaceOnUse"
            x={-pad}
            y={-pad}
            width={w + pad * 2}
            height={h + pad * 2}
        >
            <feGaussianBlur in="SourceAlpha" stdDeviation={(Number(fx.blur) || 0) / 2} result="b" />
            <feOffset in="b" dx={Number(fx.offsetX) || 0} dy={Number(fx.offsetY) || 0} result="o" />
            <feFlood floodColor={shadowColorCss(fx, 'dropShadow')} result="c" />
            <feComposite in="c" in2="o" operator="in" />
        </filter>
    );
}

/**
 * Inset shadow: invert the silhouette's alpha, blur and offset that, colour it,
 * then clip the result back to the silhouette — the standard SVG stand-in for
 * `box-shadow: inset`, which only understands rectangles.
 */
function InnerShadowFilter({ id, fx, w, h }) {
    const pad = shadowPad(fx);
    return (
        <filter
            id={id}
            filterUnits="userSpaceOnUse"
            x={-pad}
            y={-pad}
            width={w + pad * 2}
            height={h + pad * 2}
        >
            <feComponentTransfer in="SourceAlpha" result="inv">
                <feFuncA type="table" tableValues="1 0" />
            </feComponentTransfer>
            <feGaussianBlur in="inv" stdDeviation={(Number(fx.blur) || 0) / 2} result="b" />
            <feOffset in="b" dx={Number(fx.offsetX) || 0} dy={Number(fx.offsetY) || 0} result="o" />
            <feFlood floodColor={shadowColorCss(fx, 'innerShadow')} result="c" />
            <feComposite in="c" in2="o" operator="in" result="sh" />
            <feComposite in="sh" in2="SourceAlpha" operator="in" />
        </filter>
    );
}

const layer = { position: 'absolute', inset: 0, overflow: 'visible', pointerEvents: 'none' };

export default function ShapeVector({ element }) {
    const uid = useId().replace(/[^a-zA-Z0-9_-]/g, '');
    const w = Math.max(1, Number(element.width) || 1);
    const h = Math.max(1, Number(element.height) || 1);
    const s = element.styles || {};
    const d = shapePathD(element, w, h);
    if (!d) return null;

    const fill = resolveFillBackground(s, 'fill');
    const strokeW = borderWidthOf(s, SHAPE_BORDER_KEYS);
    const strokeGradient = isGradientBorder(s, SHAPE_BORDER_KEYS);
    const strokePaint = borderFillPaint(s, SHAPE_BORDER_KEYS);
    const dash = strokeDashArray(strokeGradient ? 'solid' : s.strokeStyle);
    const clip = `path("${d}")`;

    // Stored top-first, but the LAST SVG node paints on top — so reverse, exactly
    // as the export walks its shadow lists backwards.
    const drops = [...getEffects(s, 'dropShadow')].reverse();
    const inners = [...getEffects(s, 'innerShadow')].reverse();

    // CSS clips an outer shadow to OUTSIDE the silhouette, so a translucent fill
    // doesn't reveal the shadow beneath it. Even-odd against a box big enough to
    // cover every shadow reproduces that.
    const holePad = drops.reduce((max, fx) => Math.max(max, shadowPad(fx)), 0);
    const outside = `M${-holePad} ${-holePad}H${w + holePad}V${h + holePad}H${-holePad}Z${d}`;

    return (
        <div style={{ position: 'relative', width: '100%', height: '100%' }}>
            {!!drops.length && (
                <svg style={layer} width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
                    <defs>
                        <clipPath id={`${uid}-out`} clipPathUnits="userSpaceOnUse">
                            <path d={outside} clipRule="evenodd" />
                        </clipPath>
                        {drops.map((fx, i) => (
                            <DropShadowFilter key={fx.id || i} id={`${uid}-ds${i}`} fx={fx} w={w} h={h} />
                        ))}
                    </defs>
                    <g clipPath={`url(#${uid}-out)`}>
                        {drops.map((fx, i) => (
                            <path
                                key={fx.id || i}
                                d={d}
                                fill="#000"
                                // Spread grows the silhouette the shadow is cast from;
                                // a centred stroke of 2×spread dilates it by spread.
                                stroke={Number(fx.spread) > 0 ? '#000' : 'none'}
                                strokeWidth={Math.max(0, Number(fx.spread) || 0) * 2}
                                strokeLinejoin="round"
                                filter={`url(#${uid}-ds${i})`}
                            />
                        ))}
                    </g>
                </svg>
            )}

            {fill && fill !== 'transparent' && (
                <div
                    style={{
                        position: 'absolute',
                        inset: 0,
                        background: fill,
                        clipPath: clip,
                        WebkitClipPath: clip,
                    }}
                />
            )}

            {(!!inners.length || strokeW > 0) && (
                <svg style={layer} width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
                    <defs>
                        {inners.map((fx, i) => (
                            <InnerShadowFilter key={fx.id || i} id={`${uid}-is${i}`} fx={fx} w={w} h={h} />
                        ))}
                        {strokeGradient && (
                            <GradientDef id={`${uid}-sg`} paint={strokePaint} w={w} h={h} />
                        )}
                    </defs>
                    {inners.map((fx, i) => (
                        <path key={fx.id || i} d={d} fill="#000" filter={`url(#${uid}-is${i})`} />
                    ))}
                    {strokeW > 0 && (
                        <path
                            d={d}
                            fill="none"
                            stroke={strokeGradient ? `url(#${uid}-sg)` : (s.stroke || SHAPE_BORDER_KEYS.colorDefault)}
                            strokeWidth={strokeW}
                            strokeLinejoin="round"
                            strokeDasharray={dash.length ? dash.join(' ') : undefined}
                        />
                    )}
                </svg>
            )}
        </div>
    );
}
