// flubber: import via NAMESPACE + default fallback, NOT named imports. Named
// imports (`import { interpolateAll } from 'flubber'`) resolve to flubber's ESM
// `src/` entry, whose `interpolateAll` breaks under Rollup's production bundle
// (it threw → makeMorphParts fell to its joined hard cut → multi-subpath morphs
// like the logo SNAPPED, while single-path morphs via `interpolate` were fine).
// The namespace+`.default ?? ns` form resolves to flubber's bundled CJS build
// where every function works — this is exactly what the editor app does, and it
// morphs correctly there. The "default is not exported" build warning is benign.
// Import the explicit CJS build, NOT the package root. The root's `module`
// (ESM) entry resolves under Rollup to flubber's `src/` files, whose
// `interpolateAll` breaks in the production bundle (multi-subpath morphs like
// the logo SNAP). The bundled CJS build has the working implementation.
// @ts-ignore — CJS build has no type declarations; used as `any` below.
import * as flubberNs from 'flubber/build/flubber.min.js';
const flubber = flubberNs.default ?? flubberNs;
const interpolate = flubber.interpolate;
const interpolateAll = flubber.interpolateAll;
const separate = flubber.separate;
const combine = flubber.combine;
const splitPathString = flubber.splitPathString;
import { parsePath, serializePath } from './svg-path-edit';
import { drawElementToPath } from './freehand';
/**
 * Path morphing helpers shared by the editor preview and the presenter.
 *
 * The headline use is SVG→SVG (and shape→shape / shape→SVG) morphing across
 * slides: extract a single path `d` from each endpoint, then build a flubber
 * interpolator that produces an in-between `d` for any progress 0..1.
 */
/** Ellipse as 4 cubic Béziers (kappa) — editable, unlike arc commands. */
function ellipsePath(cx, cy, rx, ry) {
    const k = 0.5522847498307936;
    const ox = rx * k, oy = ry * k;
    return [
        `M${cx - rx},${cy}`,
        `C${cx - rx},${cy - oy} ${cx - ox},${cy - ry} ${cx},${cy - ry}`,
        `C${cx + ox},${cy - ry} ${cx + rx},${cy - oy} ${cx + rx},${cy}`,
        `C${cx + rx},${cy + oy} ${cx + ox},${cy + ry} ${cx},${cy + ry}`,
        `C${cx - ox},${cy + ry} ${cx - rx},${cy + oy} ${cx - rx},${cy}`,
        'Z'
    ].join(' ');
}
/** Build a `d` string for a shape type, fitted to a w×h box (origin 0,0). */
export function shapeToPath(type, w, h, borderRadius = 0) {
    const r = Math.max(0, Math.min(borderRadius, Math.min(w, h) / 2));
    switch (type) {
        case 'rectangle': {
            if (r <= 0)
                return `M0,0 L${w},0 L${w},${h} L0,${h} Z`;
            return `M${r},0 L${w - r},0 Q${w},0 ${w},${r} L${w},${h - r} Q${w},${h} ${w - r},${h} L${r},${h} Q0,${h} 0,${h - r} L0,${r} Q0,0 ${r},0 Z`;
        }
        case 'circle':
        case 'ellipse':
            return ellipsePath(w / 2, h / 2, w / 2, h / 2);
        case 'triangle':
            return `M${w / 2},0 L${w},${h} L0,${h} Z`;
        case 'hexagon': {
            // Pointy-left/right flat hexagon fitted to the box.
            const q = w * 0.25;
            return `M${q},0 L${w - q},0 L${w},${h / 2} L${w - q},${h} L${q},${h} L0,${h / 2} Z`;
        }
        case 'star': {
            const cx = w / 2, cy = h / 2, outer = Math.min(w, h) / 2, inner = outer * 0.5;
            let d = '';
            for (let i = 0; i < 10; i++) {
                const rad = i % 2 === 0 ? outer : inner;
                const ang = -Math.PI / 2 + (i * Math.PI) / 5;
                const x = cx + rad * Math.cos(ang);
                const y = cy + rad * Math.sin(ang);
                d += (i === 0 ? 'M' : 'L') + x.toFixed(2) + ',' + y.toFixed(2) + ' ';
            }
            return d + 'Z';
        }
        default:
            return `M0,0 L${w},0 L${w},${h} L0,${h} Z`;
    }
}
const NUM = '[-+]?[0-9]*\\.?[0-9]+(?:[eE][-+]?[0-9]+)?';
function nums(s) {
    return (s.match(new RegExp(NUM, 'g')) ?? []).map(Number);
}
/**
 * Convert a basic SVG primitive element to a path `d`. Returns '' for
 * unsupported tags. Handles path/rect/circle/ellipse/line/polyline/polygon.
 */
function primitiveToPath(tag, attrs) {
    const attr = (name) => {
        const m = attrs.match(new RegExp(name + '\\s*=\\s*["\']([^"\']*)["\']', 'i'));
        return m ? m[1] : null;
    };
    switch (tag.toLowerCase()) {
        case 'path':
            return attr('d') ?? '';
        case 'rect': {
            const x = Number(attr('x') ?? 0), y = Number(attr('y') ?? 0);
            const w = Number(attr('width') ?? 0), h = Number(attr('height') ?? 0);
            const rx = Number(attr('rx') ?? 0);
            if (rx > 0)
                return `M${x + rx},${y} L${x + w - rx},${y} Q${x + w},${y} ${x + w},${y + rx} L${x + w},${y + h - rx} Q${x + w},${y + h} ${x + w - rx},${y + h} L${x + rx},${y + h} Q${x},${y + h} ${x},${y + h - rx} L${x},${y + rx} Q${x},${y} ${x + rx},${y} Z`;
            return `M${x},${y} L${x + w},${y} L${x + w},${y + h} L${x},${y + h} Z`;
        }
        case 'circle': {
            const cx = Number(attr('cx') ?? 0), cy = Number(attr('cy') ?? 0), rr = Number(attr('r') ?? 0);
            return ellipsePath(cx, cy, rr, rr);
        }
        case 'ellipse': {
            const cx = Number(attr('cx') ?? 0), cy = Number(attr('cy') ?? 0);
            const rx = Number(attr('rx') ?? 0), ry = Number(attr('ry') ?? 0);
            return ellipsePath(cx, cy, rx, ry);
        }
        case 'line': {
            const [x1, y1, x2, y2] = [Number(attr('x1') ?? 0), Number(attr('y1') ?? 0), Number(attr('x2') ?? 0), Number(attr('y2') ?? 0)];
            return `M${x1},${y1} L${x2},${y2}`;
        }
        case 'polyline':
        case 'polygon': {
            const pts = nums(attr('points') ?? '');
            if (pts.length < 4)
                return '';
            let d = `M${pts[0]},${pts[1]}`;
            for (let i = 2; i < pts.length; i += 2)
                d += ` L${pts[i]},${pts[i + 1]}`;
            return tag.toLowerCase() === 'polygon' ? d + ' Z' : d;
        }
        default:
            return '';
    }
}
/**
 * Extract drawable path `d` strings from raw SVG markup. Returns an array of
 * subpath `d` strings (one per primitive). Empty when nothing is drawable.
 */
export function extractPaths(svgContent) {
    const out = [];
    const re = /<(path|rect|circle|ellipse|line|polyline|polygon)\b([^>]*)\/?>/gi;
    let m;
    while ((m = re.exec(svgContent)) !== null) {
        const d = primitiveToPath(m[1], m[2]);
        if (d.trim())
            out.push(d.trim());
    }
    return out;
}
/** Resolve a single primitive's own fill (style="fill:…" or fill="…"). */
function primitiveFill(attrs) {
    const style = attrs.match(/style\s*=\s*["']([^"']*)["']/i)?.[1];
    if (style) {
        const f = style.match(/fill\s*:\s*([^;]+)/i)?.[1]?.trim();
        if (f && f.toLowerCase() !== 'none')
            return f;
    }
    const attr = attrs.match(/\bfill\s*=\s*["']([^"']+)["']/i)?.[1]?.trim();
    if (attr && attr.toLowerCase() !== 'none')
        return attr;
    return null;
}
/**
 * Like extractPaths, but pairs each subpath with its own fill color so a
 * multi-color SVG morphs per-piece (one `<path>` per subpath) instead of
 * flattening to a single representative color. Primitives without their own
 * fill inherit the root `<svg fill="…">` (default black, matching the
 * browser), which is exactly the black outline strokes on icon SVGs.
 */
export function extractPathsWithFill(svgContent) {
    const rootFill = svgContent.match(/<svg\b[^>]*?\bfill\s*=\s*["']([^"']+)["']/i)?.[1] || '#000000';
    const out = [];
    const re = /<(path|rect|circle|ellipse|line|polyline|polygon)\b([^>]*)\/?>/gi;
    let m;
    while ((m = re.exec(svgContent)) !== null) {
        const d = primitiveToPath(m[1], m[2]);
        if (!d.trim())
            continue;
        out.push({ d: d.trim(), fill: primitiveFill(m[2]) ?? rootFill });
    }
    return out;
}
/** Single combined `d` (all subpaths concatenated) — for simple morphs. */
export function extractCombinedPath(svgContent) {
    const paths = extractPaths(svgContent);
    if (paths.length === 0)
        return null;
    return paths.join(' ');
}
/**
 * Pick a representative fill color from SVG markup — the first non-"none"
 * `fill` attribute. Used to color the flattened morph path (which loses
 * per-subpath fills). Returns null if none found.
 */
export function extractFill(svgContent) {
    // Match both `fill="x"` attributes and `fill:x` inside `style="…"` —
    // many real-world SVGs (e.g. svgrepo exports) put the color in a style
    // declaration, which the attribute-only regex would miss (→ gray morph).
    const re = /fill\s*[:=]\s*["']?\s*([^"';\s>]+)/gi;
    let m;
    while ((m = re.exec(svgContent)) !== null) {
        const v = m[1].trim();
        if (v && v.toLowerCase() !== 'none')
            return v;
    }
    return null;
}
function parseViewBox(vb) {
    const p = vb.split(/[\s,]+/).map(Number);
    return [p[0] || 0, p[1] || 0, p[2] || 1, p[3] || 1];
}
/**
 * Re-fit a path `d` from its own viewBox into a target W×H box (origin 0,0).
 * Morph stops must share a coordinate space or flubber produces scale jumps;
 * authoring normalizes every stop into the element's box via this. Parses to
 * absolute cubics, scales anchors + handles, re-serializes.
 */
export function fitPathToBox(d, sourceViewBox, targetW, targetH) {
    const [vx, vy, vw, vh] = parseViewBox(sourceViewBox);
    const sx = targetW / (vw || 1);
    const sy = targetH / (vh || 1);
    const subs = parsePath(d);
    if (!subs)
        return d;
    for (const sub of subs) {
        for (const a of sub.anchors) {
            a.x = (a.x - vx) * sx;
            a.y = (a.y - vy) * sy;
            if (a.cin) {
                a.cin.x = (a.cin.x - vx) * sx;
                a.cin.y = (a.cin.y - vy) * sy;
            }
            if (a.cout) {
                a.cout.x = (a.cout.x - vx) * sx;
                a.cout.y = (a.cout.y - vy) * sy;
            }
        }
    }
    return serializePath(subs);
}
/**
 * Resolve any vector element (shape / svg / draw) to its current geometry as
 * a path `d` plus the viewBox that `d` lives in. Single source of truth for
 * both the morph engine and the "snapshot current" authoring action.
 */
export function resolveElementPath(el) {
    if (el.type === 'shape') {
        const s = el;
        return { d: shapeToPath(s.shapeType, s.size.width, s.size.height, s.borderRadius), viewBox: `0 0 ${s.size.width} ${s.size.height}` };
    }
    if (el.type === 'svg') {
        const s = el;
        const d = extractCombinedPath(s.svgContent);
        if (!d)
            return null;
        const vb = s.viewBox ?? s.svgContent.match(/viewBox=["']([^"']+)["']/i)?.[1] ?? `0 0 ${s.size.width} ${s.size.height}`;
        return { d, viewBox: vb };
    }
    if (el.type === 'draw') {
        const p = drawElementToPath(el);
        return { d: p.d, viewBox: p.viewBox };
    }
    return null;
}
/**
 * Like resolveElementPath but keeps subpaths SEPARATE, each with its own fill,
 * so multi-color SVGs morph per-piece. Shapes/draws return a single part.
 */
export function resolveElementPathParts(el) {
    if (el.type === 'shape') {
        const s = el;
        return { parts: [{ d: shapeToPath(s.shapeType, s.size.width, s.size.height, s.borderRadius), fill: s.fillColor || '#888888' }], viewBox: `0 0 ${s.size.width} ${s.size.height}` };
    }
    if (el.type === 'svg') {
        const s = el;
        const parts = extractPathsWithFill(s.svgContent);
        if (!parts.length)
            return null;
        const vb = s.viewBox ?? s.svgContent.match(/viewBox=["']([^"']+)["']/i)?.[1] ?? `0 0 ${s.size.width} ${s.size.height}`;
        return { parts, viewBox: vb };
    }
    if (el.type === 'draw') {
        const p = drawElementToPath(el);
        return { parts: [{ d: p.d, fill: el.color }], viewBox: p.viewBox };
    }
    return null;
}
/**
 * Build per-piece morph parts between two fill-tagged subpath lists. Each `d`
 * MUST already be in the same coordinate space (caller fits the source into
 * the target viewBox). Picks the right flubber routine by count so 1↔many and
 * many↔1 map correctly, and pairs colors per piece so multi-color SVGs keep
 * their colors instead of flattening to one.
 */
export function makeMorphParts(from, to) {
    const opts = { maxSegmentLength: 10 };
    const fromN = from.length, toN = to.length;
    if (fromN === 0 || toN === 0) {
        const d = from[0]?.d || to[0]?.d || 'M0,0';
        return [{ interp: () => d, fromColor: from[0]?.fill || '#888888', toColor: to[0]?.fill || '#888888' }];
    }
    // flubber's matched interpolators give the editor's "liquid" blend (the
    // per-subpath index fallback below morphs piece-by-piece = rigid "lines").
    // These work now that the UMD-context build fix repaired interpolateAll/
    // separate/combine in the bundle; the per-subpath fallback covers any throw.
    try {
        if (fromN === 1 && toN === 1) {
            return [{ interp: interpolate(from[0].d, to[0].d, opts), fromColor: from[0].fill, toColor: to[0].fill }];
        }
        if (fromN === 1 && toN > 1) {
            const arr = separate(from[0].d, to.map((p) => p.d), opts);
            return arr.map((fn, i) => ({ interp: fn, fromColor: from[0].fill, toColor: to[i % toN].fill }));
        }
        if (fromN > 1 && toN === 1) {
            const arr = combine(from.map((p) => p.d), to[0].d, opts);
            return arr.map((fn, i) => ({ interp: fn, fromColor: from[i % fromN].fill, toColor: to[0].fill }));
        }
        if (fromN === toN) {
            const arr = interpolateAll(from.map((p) => p.d), to.map((p) => p.d), opts);
            return arr.map((fn, i) => ({ interp: fn, fromColor: from[i % fromN].fill, toColor: to[i % toN].fill }));
        }
        const merged = from.map((p) => p.d).join(' ');
        const arr = separate(merged, to.map((p) => p.d), opts);
        return arr.map((fn, i) => ({ interp: fn, fromColor: from[0].fill, toColor: to[i % toN].fill }));
    }
    catch {
        // Fallback: per-subpath index interpolation (rigid but never snaps).
        const n = Math.max(fromN, toN);
        const parts = [];
        for (let i = 0; i < n; i++) {
            const fp = from[Math.min(i, fromN - 1)];
            const tp = to[Math.min(i, toN - 1)];
            let interp;
            try {
                interp = interpolate(fp.d, tp.d, opts);
            }
            catch {
                const a = fp.d, b = tp.d;
                interp = (t) => (t < 0.5 ? a : b);
            }
            parts.push({ interp, fromColor: fp.fill, toColor: tp.fill });
        }
        return parts;
    }
}
/**
 * Build an interpolator between two `d` strings. Handles differing subpath
 * counts via flubber's many-to-many interpolation, falling back to a simple
 * single-path interpolate. Always returns a function (t:0..1) => d.
 */
export function makePathInterpolator(fromD, toD) {
    const opts = { maxSegmentLength: 10 };
    let fromParts, toParts;
    try {
        fromParts = splitPathString(fromD);
        toParts = splitPathString(toD);
    }
    catch {
        fromParts = [fromD];
        toParts = [toD];
    }
    if (fromParts.length === 0)
        fromParts = [fromD];
    if (toParts.length === 0)
        toParts = [toD];
    // Single subpath: plain `interpolate`.
    if (fromParts.length === 1 && toParts.length === 1) {
        try {
            return interpolate(fromD, toD, opts);
        }
        catch {
            return (t) => (t < 0.5 ? fromD : toD);
        }
    }
    // Multi-subpath, EQUAL counts: use interpolateAll's ARRAY form (flubber's
    // smart global matching) and concat manually. Falls back to per-subpath
    // index interpolation for unequal counts or if interpolateAll still throws.
    if (fromParts.length === toParts.length) {
        try {
            // `single: true` asks flubber to morph the multi-subpath shape as one
            // unified path. This is the same behavior used by the app preview.
            return interpolateAll(fromParts, toParts, { ...opts, single: true });
        }
        catch { /* fall through to per-subpath */ }
    }
    const n = Math.max(fromParts.length, toParts.length);
    const interps = [];
    for (let i = 0; i < n; i++) {
        const a = fromParts[Math.min(i, fromParts.length - 1)];
        const b = toParts[Math.min(i, toParts.length - 1)];
        try {
            interps.push(interpolate(a, b, opts));
        }
        catch {
            interps.push((t) => (t < 0.5 ? a : b));
        }
    }
    return (t) => interps.map((fn) => fn(t)).join(' ');
}
/**
 * Build a single interpolator that morphs through an ordered list of `d`
 * stops. `t` runs 0..1 across the whole sequence; the segment is picked by
 * scaling `t` across (stops.length - 1) and interpolating within it. Used by
 * the within-slide multi-target morph (circle → star → hexagon → …).
 * When `loop` is true the sequence wraps back to the first stop.
 */
export function makeSequenceInterpolator(stops, loop = false) {
    const seq = loop && stops.length > 1 ? [...stops, stops[0]] : stops.slice();
    if (seq.length === 0)
        return () => '';
    if (seq.length === 1)
        return () => seq[0];
    const segs = [];
    for (let i = 0; i < seq.length - 1; i++)
        segs.push(makePathInterpolator(seq[i], seq[i + 1]));
    const n = segs.length;
    return (t) => {
        const clamped = Math.max(0, Math.min(1, t));
        const scaled = clamped * n;
        let idx = Math.floor(scaled);
        if (idx >= n)
            idx = n - 1;
        const frac = scaled - idx;
        return segs[idx](frac);
    };
}
