/**
 * Editor renderer for text on a path.
 *
 * Every glyph is its own <text> at a translate+rotate taken from
 * `utils/textPathLayout` — the export replays exactly those transforms on
 * Canvas2D, which is what keeps the PNG/PDF identical. Deliberately NOT
 * <textPath>: the browser would shape and space the run itself and the export
 * could not reproduce it.
 *
 * Text shadows are shadow-only SVG filters on duplicate glyph groups behind the
 * run (`blur(r/2)` is the same Gaussian as canvas `shadowBlur = r`), so a shadow
 * follows the letterforms rather than a box.
 */
import { useId } from 'react';
import {
    getEffects, shadowColorCss, buildTextStroke, resolveFontStack,
    isGradientText, textFillPaint, spanFillPaint,
} from '../panels/propertyControls';
import { layoutTextOnPath, textPathD } from '../utils/textPathLayout';
import { elementColorSpans, paintAtIndex } from '../utils/textRuns';
import { sampleGradientAt } from '../utils/gradientSample';

const FIGMA_BLUE = '#0d99ff';

function ShadowFilter({ id, fx, w, h, pad }) {
    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, 'textShadow')} result="c" />
            <feComposite in="c" in2="o" operator="in" />
        </filter>
    );
}

/**
 * Text caret, drawn on the curve at a glyph boundary. Live editing hides the real
 * textarea caret (the textarea itself is transparent), so the run draws its own —
 * rotated with the tangent, sitting on the same baseline as the letters.
 *
 * `index` is an offset into the element's CONTENT, not a position in the glyph
 * array: a path run collapses line breaks, so the two only coincide in text with
 * no newlines. Every consumer of a caret or a selection speaks content offsets
 * (that is what the panel colours), so the glyph is looked up by `g.index`.
 */
function Caret({ glyphs, index, fontSize }) {
    if (!glyphs.length) return null;
    const at = glyphs.findIndex((g) => g.index >= index);
    const atEnd = at === -1;
    const g = glyphs[atEnd ? glyphs.length - 1 : at];
    const x = atEnd ? g.advance / 2 : -g.advance / 2;
    return (
        <line
            className="fig-caret"
            transform={`translate(${g.x} ${g.y}) rotate(${g.angle})`}
            x1={x}
            y1={-fontSize * 0.82}
            x2={x}
            y2={fontSize * 0.18}
            stroke={FIGMA_BLUE}
            strokeWidth={1.5}
        />
    );
}

/**
 * The selected characters, highlighted ON the curve — one rotated band per glyph,
 * so the highlight follows the letters instead of the flat box the hidden
 * textarea lays them out in. Bands are drawn a touch wide so neighbours meet.
 */
function SelectionBands({ glyphs, range, fontSize }) {
    if (!range || range.end <= range.start) return null;
    const hit = glyphs.filter((g) => g.index >= range.start && g.index < range.end);
    if (!hit.length) return null;
    return hit.map((g, i) => (
        <rect
            key={`${g.index}:${i}`}
            transform={`translate(${g.x} ${g.y}) rotate(${g.angle})`}
            x={-g.advance / 2 - 0.5}
            y={-fontSize * 0.82}
            width={g.advance + 1}
            height={fontSize}
            fill={FIGMA_BLUE}
            opacity={0.28}
        />
    ));
}

export default function TextOnPath({
    element, showGuide = false, caret = null, selection = null,
    // Letters on a curve routinely stand OUTSIDE the box, and the box is the only
    // thing the canvas wrapper makes clickable — so the ink itself has to be
    // hit-testable or the only way to select is to find the empty frame. Off while
    // editing, where a click on a far-flung glyph should blur the field instead of
    // starting a drag.
    interactive = true,
}) {
    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 styles = element.styles || {};
    // An empty box still has to show a caret while it is being typed into, so lay a
    // single space out instead — it draws nothing but gives the caret a point.
    const source = (caret != null && !element.content) ? { ...element, content: ' ' } : element;
    const laid = layoutTextOnPath(source, w, h);
    if (!laid) return null;

    const fontSize = Number(styles.fontSize) || 16;
    const stroke = buildTextStroke(styles);
    const spans = elementColorSpans(element, element.content);
    const baseFill = styles.color || '#18181b';
    const gradient = isGradientText(styles) ? textFillPaint(styles) : null;
    // Stored top-first; the last SVG node paints on top, so reverse to match CSS.
    const shadows = [...getEffects(styles, 'textShadow')].reverse();
    const pad = Math.ceil(
        fontSize * 2
        + shadows.reduce((max, fx) => Math.max(
            max,
            (Number(fx.blur) || 0) + Math.abs(Number(fx.offsetX) || 0) + Math.abs(Number(fx.offsetY) || 0),
        ), 0),
    );

    // 'auto' on the glyph groups overrides the SVG root's pointer-events:none, and
    // SVG hit-tests painted ink only — so a click lands on a LETTER, not on the
    // rectangle around it, and it bubbles to the canvas wrapper's drag/select
    // handlers like any other part of the element.
    const hit = interactive ? 'auto' : 'none';

    const glyphProps = {
        fontFamily: resolveFontStack(styles),
        fontSize,
        fontWeight: styles.fontWeight || 400,
        fontStyle: styles.fontStyle || 'normal',
        // The wrapper sets the move cursor; inherit it so hovering a letter doesn't
        // flip to a text I-beam that means nothing here.
        style: { cursor: 'inherit' },
    };

    // A glyph's own colour beats the run's: a coloured character first, then the
    // element's gradient sampled where that letter sits (a CanvasGradient/SVG
    // gradient would be re-transformed per glyph and shear), then the plain fill.
    const glyphFill = (g, fill) => {
        // The shadow and stroke passes draw the same glyphs in their own colour —
        // only the fill pass is the text's colour, so only it is overridden.
        if (fill !== baseFill) return fill;
        const own = paintAtIndex(spans, g.index);
        // A run's own gradient is sampled per glyph too, for the same reason the
        // element's is: an SVG gradient would be re-transformed by each glyph.
        if (own) return sampleGradientAt(spanFillPaint(own), g.x, g.y, w, h) || own.color;
        if (!gradient) return fill;
        return sampleGradientAt(gradient, g.x, g.y, w, h) || fill;
    };

    const run = (fill, extra = {}) => laid.glyphs.map((g, i) => (
        <text
            // Glyphs repeat, so the index is the only stable key here.
            key={i}
            transform={`translate(${g.x} ${g.y}) rotate(${g.angle})`}
            x={-g.advance / 2}
            y={0}
            fill={glyphFill(g, fill)}
            {...glyphProps}
            {...extra}
        >
            {g.ch === ' ' ? ' ' : g.ch}
        </text>
    ));

    return (
        <svg
            width={w}
            height={h}
            viewBox={`0 0 ${w} ${h}`}
            style={{ position: 'absolute', inset: 0, overflow: 'visible', pointerEvents: 'none' }}
        >
            {!!shadows.length && (
                <defs>
                    {shadows.map((fx, i) => (
                        <ShadowFilter key={fx.id || i} id={`${uid}-ts${i}`} fx={fx} w={w} h={h} pad={pad} />
                    ))}
                </defs>
            )}
            {/* The path itself is editor chrome — selection only, never exported. */}
            {showGuide && (
                <path
                    d={textPathD(element, w, h)}
                    fill="none"
                    stroke={FIGMA_BLUE}
                    strokeWidth={1}
                    strokeDasharray="4 3"
                    opacity={0.9}
                    vectorEffect="non-scaling-stroke"
                />
            )}
            {/* Under the run, like a browser's own selection highlight. */}
            <SelectionBands glyphs={laid.glyphs} range={selection} fontSize={fontSize} />
            {shadows.map((fx, i) => (
                <g key={fx.id || i} filter={`url(#${uid}-ts${i})`}>{run('#000')}</g>
            ))}
            {/* An outside stroke paints UNDER the fill so the fill hides its inner
                half — the same trick `paint-order: stroke fill` plays in CSS. */}
            {stroke && stroke.paintOrder && (
                <g pointerEvents={hit}>
                    {run('none', { stroke: stroke.color, strokeWidth: stroke.width, strokeLinejoin: 'miter' })}
                </g>
            )}
            <g pointerEvents={hit}>{run(baseFill)}</g>
            {stroke && !stroke.paintOrder && (
                <g pointerEvents={hit}>
                    {run('none', { stroke: stroke.color, strokeWidth: stroke.width, strokeLinejoin: 'miter' })}
                </g>
            )}
            {caret != null && <Caret glyphs={laid.glyphs} index={caret} fontSize={fontSize} />}
        </svg>
    );
}
