/**
 * Parse/serialize an SVG path `d` into an editable cubic-Bézier anchor model
 * used by the node editor. Every segment is normalized to a cubic so each
 * anchor has at most an incoming + outgoing control handle — the same mental
 * model as the motion-path editor.
 *
 * Supported input commands: M m L l H h V v C c S s Q q T t Z z.
 * Arcs (A/a) are not supported; `parsePath` returns null so the caller can
 * keep node-editing disabled for those paths.
 */
const TOKEN = /([MmLlHhVvCcSsQqTtZzAa])|(-?\d*\.?\d+(?:e[-+]?\d+)?)/gi;
export function parsePath(d) {
    const tokens = [];
    let m;
    while ((m = TOKEN.exec(d)) !== null) {
        if (m[1])
            tokens.push(m[1]);
        else
            tokens.push(parseFloat(m[2]));
    }
    const subs = [];
    let cur = null;
    let px = 0, py = 0; // current point
    let startX = 0, startY = 0; // subpath start
    let prevCubicC2 = null; // for S
    let prevQuadC = null; // for T
    let i = 0;
    let cmd = '';
    const num = () => tokens[i++];
    const isCmd = (t) => typeof t === 'string';
    const pushAnchor = (x, y) => {
        cur.anchors.push({ x, y });
    };
    while (i < tokens.length) {
        if (isCmd(tokens[i])) {
            cmd = tokens[i];
            i++;
        }
        const rel = cmd === cmd.toLowerCase();
        const c = cmd.toUpperCase();
        if (c === 'M') {
            const x = num() + (rel ? px : 0);
            const y = num() + (rel ? py : 0);
            cur = { anchors: [], closed: false };
            subs.push(cur);
            pushAnchor(x, y);
            px = startX = x;
            py = startY = y;
            prevCubicC2 = prevQuadC = null;
            cmd = rel ? 'l' : 'L'; // subsequent pairs are implicit lineto
        }
        else if (c === 'L') {
            const x = num() + (rel ? px : 0);
            const y = num() + (rel ? py : 0);
            pushAnchor(x, y);
            px = x;
            py = y;
            prevCubicC2 = prevQuadC = null;
        }
        else if (c === 'H') {
            const x = num() + (rel ? px : 0);
            pushAnchor(x, py);
            px = x;
            prevCubicC2 = prevQuadC = null;
        }
        else if (c === 'V') {
            const y = num() + (rel ? py : 0);
            pushAnchor(px, y);
            py = y;
            prevCubicC2 = prevQuadC = null;
        }
        else if (c === 'C' || c === 'S') {
            let c1x, c1y;
            if (c === 'C') {
                c1x = num() + (rel ? px : 0);
                c1y = num() + (rel ? py : 0);
            }
            else {
                // S: first control = reflection of previous cubic's 2nd control.
                c1x = prevCubicC2 ? 2 * px - prevCubicC2.x : px;
                c1y = prevCubicC2 ? 2 * py - prevCubicC2.y : py;
            }
            const c2x = num() + (rel ? px : 0), c2y = num() + (rel ? py : 0);
            const x = num() + (rel ? px : 0), y = num() + (rel ? py : 0);
            if (cur.anchors.length)
                cur.anchors[cur.anchors.length - 1].cout = { x: c1x, y: c1y };
            cur.anchors.push({ x, y, cin: { x: c2x, y: c2y } });
            prevCubicC2 = { x: c2x, y: c2y };
            prevQuadC = null;
            px = x;
            py = y;
        }
        else if (c === 'Q' || c === 'T') {
            let qx, qy;
            if (c === 'Q') {
                qx = num() + (rel ? px : 0);
                qy = num() + (rel ? py : 0);
            }
            else {
                qx = prevQuadC ? 2 * px - prevQuadC.x : px;
                qy = prevQuadC ? 2 * py - prevQuadC.y : py;
            }
            const x = num() + (rel ? px : 0), y = num() + (rel ? py : 0);
            // Quadratic → cubic control points.
            const c1x = px + (2 / 3) * (qx - px), c1y = py + (2 / 3) * (qy - py);
            const c2x = x + (2 / 3) * (qx - x), c2y = y + (2 / 3) * (qy - y);
            if (cur.anchors.length)
                cur.anchors[cur.anchors.length - 1].cout = { x: c1x, y: c1y };
            cur.anchors.push({ x, y, cin: { x: c2x, y: c2y } });
            prevQuadC = { x: qx, y: qy };
            prevCubicC2 = null;
            px = x;
            py = y;
        }
        else if (c === 'A') {
            // Elliptical arc → one or more cubic Béziers (so the rest of the
            // editor only ever deals with anchors + cubic handles).
            const rx = num(), ry = num(), xrot = num(), large = num(), sweep = num();
            const ex = num() + (rel ? px : 0), ey = num() + (rel ? py : 0);
            const cubics = arcToCubics(px, py, rx, ry, xrot, large, sweep, ex, ey);
            for (const seg of cubics) {
                if (cur.anchors.length)
                    cur.anchors[cur.anchors.length - 1].cout = { x: seg.c1x, y: seg.c1y };
                cur.anchors.push({ x: seg.x, y: seg.y, cin: { x: seg.c2x, y: seg.c2y } });
            }
            px = ex;
            py = ey;
            prevCubicC2 = prevQuadC = null;
        }
        else if (c === 'Z') {
            if (cur)
                cur.closed = true;
            px = startX;
            py = startY;
            prevCubicC2 = prevQuadC = null;
        }
        else {
            // Unknown — bail.
            return null;
        }
    }
    return subs.filter((s) => s.anchors.length > 0);
}
/**
 * Reduce anchor count via Ramer–Douglas–Peucker on anchor positions. Used to
 * make a dense freehand outline node-editable (otherwise it has hundreds of
 * handles). Kept anchors retain their Bézier handles; dropped ones become
 * straight. `tolerance` is in the path's own coordinate units.
 */
export function simplifyAnchors(subs, tolerance) {
    const rdp = (pts) => {
        if (pts.length < 3)
            return pts;
        let maxD = 0, idx = 0;
        const a = pts[0], b = pts[pts.length - 1];
        const dx = b.x - a.x, dy = b.y - a.y;
        const len = Math.hypot(dx, dy) || 1;
        for (let i = 1; i < pts.length - 1; i++) {
            const d = Math.abs((pts[i].x - a.x) * dy - (pts[i].y - a.y) * dx) / len;
            if (d > maxD) {
                maxD = d;
                idx = i;
            }
        }
        if (maxD > tolerance) {
            const left = rdp(pts.slice(0, idx + 1));
            const right = rdp(pts.slice(idx));
            return left.slice(0, -1).concat(right);
        }
        return [a, b];
    };
    return subs.map((s) => ({ closed: s.closed, anchors: rdp(s.anchors) }));
}
/** Convert an SVG elliptical arc to a list of cubic Bézier segments. */
function arcToCubics(x1, y1, rx, ry, xRotDeg, large, sweep, x2, y2) {
    if (rx === 0 || ry === 0)
        return [{ c1x: x1, c1y: y1, c2x: x2, c2y: y2, x: x2, y: y2 }];
    rx = Math.abs(rx);
    ry = Math.abs(ry);
    const phi = (xRotDeg * Math.PI) / 180;
    const cosP = Math.cos(phi), sinP = Math.sin(phi);
    // Step 1: transform to ellipse-centered coords.
    const dx = (x1 - x2) / 2, dy = (y1 - y2) / 2;
    const x1p = cosP * dx + sinP * dy;
    const y1p = -sinP * dx + cosP * dy;
    // Correct out-of-range radii.
    let lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
    if (lambda > 1) {
        const s = Math.sqrt(lambda);
        rx *= s;
        ry *= s;
    }
    let sign = large === sweep ? -1 : 1;
    let num = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
    let den = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
    let co = sign * Math.sqrt(Math.max(0, num / den));
    const cxp = (co * rx * y1p) / ry;
    const cyp = (-co * ry * x1p) / rx;
    const cx = cosP * cxp - sinP * cyp + (x1 + x2) / 2;
    const cy = sinP * cxp + cosP * cyp + (y1 + y2) / 2;
    const ang = (ux, uy, vx, vy) => {
        const dot = ux * vx + uy * vy;
        const len = Math.hypot(ux, uy) * Math.hypot(vx, vy);
        let a = Math.acos(Math.max(-1, Math.min(1, dot / len)));
        if (ux * vy - uy * vx < 0)
            a = -a;
        return a;
    };
    let theta1 = ang(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
    let dTheta = ang((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
    if (!sweep && dTheta > 0)
        dTheta -= 2 * Math.PI;
    if (sweep && dTheta < 0)
        dTheta += 2 * Math.PI;
    const segCount = Math.max(1, Math.ceil(Math.abs(dTheta) / (Math.PI / 2)));
    const delta = dTheta / segCount;
    const t = (4 / 3) * Math.tan(delta / 4);
    const out = [];
    let th = theta1;
    let curX = x1, curY = y1;
    for (let i = 0; i < segCount; i++) {
        const th2 = th + delta;
        const cosTh = Math.cos(th), sinTh = Math.sin(th);
        const cosTh2 = Math.cos(th2), sinTh2 = Math.sin(th2);
        // Derivatives in ellipse space → control points, rotated back.
        const ep = (ca, sa) => ({
            x: cx + rx * cosP * ca - ry * sinP * sa,
            y: cy + rx * sinP * ca + ry * cosP * sa
        });
        const d1 = { x: -rx * cosP * sinTh - ry * sinP * cosTh, y: -rx * sinP * sinTh + ry * cosP * cosTh };
        const d2 = { x: -rx * cosP * sinTh2 - ry * sinP * cosTh2, y: -rx * sinP * sinTh2 + ry * cosP * cosTh2 };
        const p2 = ep(cosTh2, sinTh2);
        out.push({
            c1x: curX + t * d1.x, c1y: curY + t * d1.y,
            c2x: p2.x - t * d2.x, c2y: p2.y - t * d2.y,
            x: p2.x, y: p2.y
        });
        curX = p2.x;
        curY = p2.y;
        th = th2;
    }
    return out;
}
function n(v) {
    return Number.isInteger(v) ? String(v) : v.toFixed(2);
}
export function serializePath(subs) {
    const parts = [];
    for (const sub of subs) {
        const a = sub.anchors;
        if (a.length === 0)
            continue;
        parts.push(`M${n(a[0].x)},${n(a[0].y)}`);
        for (let k = 1; k < a.length; k++) {
            const prev = a[k - 1];
            const cur = a[k];
            if (prev.cout || cur.cin) {
                const c1 = prev.cout ?? { x: prev.x, y: prev.y };
                const c2 = cur.cin ?? { x: cur.x, y: cur.y };
                parts.push(`C${n(c1.x)},${n(c1.y)} ${n(c2.x)},${n(c2.y)} ${n(cur.x)},${n(cur.y)}`);
            }
            else {
                parts.push(`L${n(cur.x)},${n(cur.y)}`);
            }
        }
        if (sub.closed) {
            // Curve back to start if the closing edge had handles.
            const last = a[a.length - 1];
            const first = a[0];
            if (last.cout || first.cin) {
                const c1 = last.cout ?? { x: last.x, y: last.y };
                const c2 = first.cin ?? { x: first.x, y: first.y };
                parts.push(`C${n(c1.x)},${n(c1.y)} ${n(c2.x)},${n(c2.y)} ${n(first.x)},${n(first.y)}`);
            }
            parts.push('Z');
        }
    }
    return parts.join(' ');
}
