import * as P from '@react-pdf/primitives';
import { isNil, matchPercent } from '@react-pdf/fns';
import absPath from 'abs-svg-path';
import parsePath from 'parse-svg-path';
import normalizePath from 'normalize-svg-path';
import colorString from 'color-string';

const renderPath = (ctx, node) => {
    const d = node.props?.d;
    if (d)
        ctx.path(node.props.d);
};

const KAPPA$3 = 4.0 * ((Math.sqrt(2) - 1.0) / 3.0);
const renderRect = (ctx, node) => {
    const x = node.props?.x || 0;
    const y = node.props?.y || 0;
    const rx = node.props?.rx || 0;
    const ry = node.props?.ry || 0;
    const width = node.props?.width || 0;
    const height = node.props?.height || 0;
    if (!width || !height)
        return;
    if (rx && ry) {
        const krx = rx * KAPPA$3;
        const kry = ry * KAPPA$3;
        ctx.moveTo(x + rx, y);
        ctx.lineTo(x - rx + width, y);
        ctx.bezierCurveTo(x - rx + width + krx, y, x + width, y + ry - kry, x + width, y + ry);
        ctx.lineTo(x + width, y + height - ry);
        ctx.bezierCurveTo(x + width, y + height - ry + kry, x - rx + width + krx, y + height, x - rx + width, y + height);
        ctx.lineTo(x + rx, y + height);
        ctx.bezierCurveTo(x + rx - krx, y + height, x, y + height - ry + kry, x, y + height - ry);
        ctx.lineTo(x, y + ry);
        ctx.bezierCurveTo(x, y + ry - kry, x + rx - krx, y, x + rx, y);
    }
    else {
        ctx.moveTo(x, y);
        ctx.lineTo(x + width, y);
        ctx.lineTo(x + width, y + height);
        ctx.lineTo(x, y + height);
    }
    ctx.closePath();
};

const renderLine$1 = (ctx, node) => {
    const { x1, x2, y1, y2 } = node.props || {};
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
};

const renderGroup = () => {
    // noop
};

const KAPPA$2 = 4.0 * ((Math.sqrt(2) - 1.0) / 3.0);
const drawEllipse = (ctx, rx, ry, cx = 0, cy = 0) => {
    const x = cx - rx;
    const y = cy - ry;
    const ox = rx * KAPPA$2;
    const oy = ry * KAPPA$2;
    const xe = x + rx * 2;
    const ye = y + ry * 2;
    const xm = x + rx;
    const ym = y + ry;
    ctx.moveTo(x, ym);
    ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
    ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
    ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
    ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
    ctx.closePath();
};
const renderEllipse = (ctx, node) => {
    const { cx, cy, rx, ry } = node.props || {};
    drawEllipse(ctx, rx, ry, cx, cy);
};

const renderCircle = (ctx, node) => {
    const cx = node.props?.cx;
    const cy = node.props?.cy;
    const r = node.props?.r;
    drawEllipse(ctx, r, r, cx, cy);
};

/* eslint-disable no-return-assign */
const number = (n) => {
    if (n > -1e21 && n < 1e21) {
        return Math.round(n * 1e6) / 1e6;
    }
    throw new Error(`unsupported number: ${n}`);
};
const _renderGlyphs = (ctx, encoded, positions, x, y) => {
    const commands = [];
    const scale = ctx._fontSize / 1000;
    let i;
    let last = 0;
    let hadOffset = false;
    ctx.save();
    // flip coordinate system
    ctx.transform(1, 0, 0, -1, 0, ctx.page.height);
    y = ctx.page.height - y;
    // add current font to page if necessary
    if (ctx.page.fonts[ctx._font.id] == null) {
        ctx.page.fonts[ctx._font.id] = ctx._font.ref();
    }
    // begin the text object
    ctx.addContent('BT');
    // text position
    ctx.addContent(`1 0 0 1 ${number(x)} ${number(y)} Tm`);
    // font and font size
    ctx.addContent(`/${ctx._font.id} ${number(ctx._fontSize)} Tf`);
    // Adds a segment of text to the TJ command buffer
    const addSegment = (cur) => {
        if (last < cur) {
            const hex = encoded.slice(last, cur).join('');
            const advance = positions[cur - 1].xAdvance - positions[cur - 1].advanceWidth;
            commands.push(`<${hex}> ${number(-advance)}`);
        }
        return (last = cur);
    };
    // Flushes the current TJ commands to the output stream
    const flush = (s) => {
        addSegment(s);
        if (commands.length > 0) {
            ctx.addContent(`[${commands.join(' ')}] TJ`);
            return (commands.length = 0);
        }
    };
    for (i = 0; i < positions.length; i += 1) {
        // If we have an x or y offset, we have to break out of the current TJ command
        // so we can move the text position.
        const pos = positions[i];
        if (pos.xOffset || pos.yOffset) {
            // Flush the current buffer
            flush(i);
            // Move the text position and flush just the current character
            ctx.addContent(`1 0 0 1 ${number(x + pos.xOffset * scale)} ${number(y + pos.yOffset * scale)} Tm`);
            flush(i + 1);
            hadOffset = true;
        }
        else {
            // If the last character had an offset, reset the text position
            if (hadOffset) {
                ctx.addContent(`1 0 0 1 ${number(x)} ${number(y)} Tm`);
                hadOffset = false;
            }
            // Group segments that don't have any advance adjustments
            if (pos.xAdvance - pos.advanceWidth !== 0) {
                addSegment(i + 1);
            }
        }
        x += pos.xAdvance * scale;
    }
    // Flush any remaining commands
    flush(i);
    // end the text object
    ctx.addContent('ET');
    // restore flipped coordinate system
    return ctx.restore();
};
const renderGlyphs = (ctx, glyphs, positions, x, y) => {
    const scale = 1000 / ctx._fontSize;
    const unitsPerEm = ctx._font.font.unitsPerEm || 1000;
    const advanceWidthScale = 1000 / unitsPerEm;
    // Glyph encoding and positioning
    const encodedGlyphs = ctx._font.encodeGlyphs(glyphs);
    const encodedPositions = positions.map((pos, i) => ({
        xAdvance: pos.xAdvance * scale,
        yAdvance: pos.yAdvance * scale,
        xOffset: pos.xOffset,
        yOffset: pos.yOffset,
        advanceWidth: glyphs[i].advanceWidth * advanceWidthScale,
    }));
    return _renderGlyphs(ctx, encodedGlyphs, encodedPositions, x, y);
};

const renderRun$1 = (ctx, run) => {
    if (!run.glyphs)
        return;
    if (!run.positions)
        return;
    const runAdvanceWidth = run.xAdvance;
    const font = run.attributes.font?.[0];
    const { fontSize, color, opacity } = run.attributes;
    if (color)
        ctx.fillColor(color);
    ctx.fillOpacity(opacity);
    if (font) {
        ctx.font(font.type === 'STANDARD' ? font.fullName : font, fontSize);
    }
    try {
        renderGlyphs(ctx, run.glyphs, run.positions, 0, 0);
    }
    catch (error) {
        console.log(error);
    }
    ctx.translate(runAdvanceWidth, 0);
};
const renderSpan = (ctx, line, textAnchor, dominantBaseline) => {
    ctx.save();
    const x = line.box?.x || 0;
    const y = line.box?.y || 0;
    const font = line.runs[0]?.attributes.font?.[0];
    const scale = line.runs[0]?.attributes?.scale || 1;
    const width = line.xAdvance;
    if (!font)
        return;
    const ascent = font.ascent * scale;
    const xHeight = font.xHeight * scale;
    const descent = font.descent * scale;
    const capHeight = font.capHeight * scale;
    let xTranslate = x;
    let yTranslate = y;
    switch (textAnchor) {
        case 'middle':
            xTranslate = x - width / 2;
            break;
        case 'end':
            xTranslate = x - width;
            break;
        default:
            xTranslate = x;
            break;
    }
    switch (dominantBaseline) {
        case 'middle':
        case 'central':
            yTranslate = y + capHeight / 2;
            break;
        case 'hanging':
            yTranslate = y + capHeight;
            break;
        case 'mathematical':
            yTranslate = y + xHeight;
            break;
        case 'text-after-edge':
            yTranslate = y + descent;
            break;
        case 'text-before-edge':
            yTranslate = y + ascent;
            break;
        default:
            yTranslate = y;
            break;
    }
    ctx.translate(xTranslate, yTranslate);
    line.runs.forEach((run) => renderRun$1(ctx, run));
    ctx.restore();
};
const renderSvgText = (ctx, node) => {
    const children = node.children;
    children.forEach((span) => renderSpan(ctx, span.lines[0], span.props.textAnchor, span.props.dominantBaseline));
};

const pairs = (values) => {
    const result = [];
    for (let i = 0; i < values.length; i += 2) {
        result.push([values[i], values[i + 1]]);
    }
    return result;
};
/**
 * Parse svg-like points into number arrays
 *
 * @param points string ex. "20,30 50,60"
 * @returns points array ex. [[20, 30], [50, 60]]
 */
const parsePoints = (points) => {
    let values = (points || '')
        .trim()
        .replace(/,/g, ' ')
        .replace(/(\d)-(\d)/g, '$1 -$2')
        .split(/\s+/);
    if (values.length % 2 !== 0) {
        values = values.slice(0, -1);
    }
    const mappedValues = values.map(parseFloat);
    return pairs(mappedValues);
};

const drawPolyline = (ctx, points) => {
    if (points.length > 0) {
        ctx.moveTo(points[0][0], points[0][1]);
        points.slice(1).forEach((p) => ctx.lineTo(p[0], p[1]));
    }
};
const renderPolyline = (ctx, node) => {
    const points = parsePoints(node.props.points || '');
    drawPolyline(ctx, points);
};

const renderPolygon = (ctx, node) => {
    const points = parsePoints(node.props.points || '');
    drawPolyline(ctx, points);
    ctx.closePath();
};

const renderImage$1 = (ctx, node) => {
    if (!node.box)
        return;
    if (!node.image?.data)
        return;
    const { x = 0, y = 0 } = node.props;
    const { width, height, opacity } = node.style;
    const paddingTop = node.box.paddingLeft || 0;
    const paddingLeft = node.box.paddingLeft || 0;
    if (width === 0 || height === 0) {
        console.warn(`Image with src '${node.props.href}' skipped due to invalid dimensions`);
        return;
    }
    if (typeof width === 'string' || typeof height === 'string') {
        console.warn(`Image with src '${node.props.href}' skipped due to percentage width or height`);
        return;
    }
    ctx.save();
    ctx
        .fillOpacity(opacity || 1)
        .image(node.image.data, x + paddingLeft, y + paddingTop, {
        width,
        height,
    });
    ctx.restore();
};

// This constant is used to approximate a symmetrical arc using a cubic
// Bezier curve.
const KAPPA$1 = 4.0 * ((Math.sqrt(2) - 1.0) / 3.0);
const clipNode = (ctx, node) => {
    if (!node.box)
        return;
    if (!node.style)
        return;
    const { top, left, width, height } = node.box;
    const { borderTopLeftRadius = 0, borderTopRightRadius = 0, borderBottomRightRadius = 0, borderBottomLeftRadius = 0, } = node.style;
    // Border top
    // @ts-expect-error this is always a number due to resolve border radius step
    const rtr = Math.min(borderTopRightRadius, 0.5 * width, 0.5 * height);
    const ctr = rtr * (1.0 - KAPPA$1);
    ctx.moveTo(left + rtr, top);
    ctx.lineTo(left + width - rtr, top);
    ctx.bezierCurveTo(left + width - ctr, top, left + width, top + ctr, left + width, top + rtr);
    // Border right
    // @ts-expect-error this is always a number due to resolve border radius step
    const rbr = Math.min(borderBottomRightRadius, 0.5 * width, 0.5 * height);
    const cbr = rbr * (1.0 - KAPPA$1);
    ctx.lineTo(left + width, top + height - rbr);
    ctx.bezierCurveTo(left + width, top + height - cbr, left + width - cbr, top + height, left + width - rbr, top + height);
    // Border bottom
    // @ts-expect-error this is always a number due to resolve border radius step
    const rbl = Math.min(borderBottomLeftRadius, 0.5 * width, 0.5 * height);
    const cbl = rbl * (1.0 - KAPPA$1);
    ctx.lineTo(left + rbl, top + height);
    ctx.bezierCurveTo(left + cbl, top + height, left, top + height - cbl, left, top + height - rbl);
    // Border left
    // @ts-expect-error this is always a number due to resolve border radius step
    const rtl = Math.min(borderTopLeftRadius, 0.5 * width, 0.5 * height);
    const ctl = rtl * (1.0 - KAPPA$1);
    ctx.lineTo(left, top + rtl);
    ctx.bezierCurveTo(left, top + ctl, left + ctl, top, left + rtl, top);
    ctx.closePath();
    ctx.clip();
};

const applySingleTransformation = (ctx, transform, origin) => {
    const { operation, value } = transform;
    switch (operation) {
        case 'scale': {
            const [scaleX, scaleY] = value;
            ctx.scale(scaleX, scaleY, { origin });
            break;
        }
        case 'rotate': {
            const [angle] = value;
            ctx.rotate(angle, { origin });
            break;
        }
        case 'translate': {
            const [x, y = 0] = value;
            ctx.translate(x, y, { origin });
            break;
        }
        case 'skew': {
            const [xAngle = 0, yAngle = 0] = value;
            const radx = (xAngle * Math.PI) / 180;
            const rady = (yAngle * Math.PI) / 180;
            const tanx = Math.tan(radx);
            const tany = Math.tan(rady);
            let x = 0;
            let y = 0;
            if (origin != null) {
                [x, y] = Array.from(origin);
                const x1 = x + tanx * y;
                const y1 = y + tany * x;
                x -= x1;
                y -= y1;
            }
            ctx.transform(1, tany, tanx, 1, x, y);
            break;
        }
        case 'matrix': {
            ctx.transform(...value);
            break;
        }
        default: {
            console.error(`Transform operation: '${operation}' doesn't supported`);
        }
    }
};
const applyTransformations = (ctx, node) => {
    if (!node.origin)
        return;
    const { props, style } = node;
    const origin = [node.origin.left, node.origin.top];
    const propsTransform = 'transform' in props ? props.transform : undefined;
    const operations = style?.transform || propsTransform || [];
    operations.forEach((operation) => {
        applySingleTransformation(ctx, operation, origin);
    });
};

// From https://github.com/dy/svg-path-bounds/blob/master/index.js
const getPathBoundingBox = (node) => {
    const path = normalizePath(absPath(parsePath(node.props?.d || '')));
    if (!path.length)
        return [0, 0, 0, 0];
    const bounds = [Infinity, Infinity, -Infinity, -Infinity];
    for (let i = 0, l = path.length; i < l; i += 1) {
        const points = path[i].slice(1);
        for (let j = 0; j < points.length; j += 2) {
            if (points[j + 0] < bounds[0])
                bounds[0] = points[j + 0];
            if (points[j + 1] < bounds[1])
                bounds[1] = points[j + 1];
            if (points[j + 0] > bounds[2])
                bounds[2] = points[j + 0];
            if (points[j + 1] > bounds[3])
                bounds[3] = points[j + 1];
        }
    }
    return bounds;
};
const getCircleBoundingBox = (node) => {
    const r = node.props?.r || 0;
    const cx = node.props?.cx || 0;
    const cy = node.props?.cy || 0;
    return [cx - r, cy - r, cx + r, cy + r];
};
const getEllipseBoundingBox = (node) => {
    const cx = node.props?.cx || 0;
    const cy = node.props?.cy || 0;
    const rx = node.props?.rx || 0;
    const ry = node.props?.ry || 0;
    return [cx - rx, cy - ry, cx + rx, cy + ry];
};
const getLineBoundingBox = (node) => {
    const x1 = node.props?.x1 || 0;
    const y1 = node.props?.y1 || 0;
    const x2 = node.props?.x2 || 0;
    const y2 = node.props?.y2 || 0;
    return [
        Math.min(x1, x2),
        Math.min(y1, y2),
        Math.max(x1, x2),
        Math.max(y1, y2),
    ];
};
const getRectBoundingBox = (node) => {
    const x = node.props?.x || 0;
    const y = node.props?.y || 0;
    const width = node.props?.width || 0;
    const height = node.props?.height || 0;
    return [x, y, x + width, y + height];
};
const max = (values) => Math.max(-Infinity, ...values);
const min = (values) => Math.min(Infinity, ...values);
const getPolylineBoundingBox = (node) => {
    const points = parsePoints(node.props?.points);
    const xValues = points.map((p) => p[0]);
    const yValues = points.map((p) => p[1]);
    return [min(xValues), min(yValues), max(xValues), max(yValues)];
};
const boundingBoxFns = {
    [P.Rect]: getRectBoundingBox,
    [P.Line]: getLineBoundingBox,
    [P.Path]: getPathBoundingBox,
    [P.Circle]: getCircleBoundingBox,
    [P.Ellipse]: getEllipseBoundingBox,
    [P.Polygon]: getPolylineBoundingBox,
    [P.Polyline]: getPolylineBoundingBox,
};
const getBoundingBox = (node) => {
    const boundingBoxFn = boundingBoxFns[node.type];
    return boundingBoxFn ? boundingBoxFn(node) : [0, 0, 0, 0];
};

const setStrokeWidth = (ctx, node) => {
    if (!node.props)
        return;
    if (!('strokeWidth' in node.props))
        return;
    const lineWidth = node.props.strokeWidth;
    if (lineWidth)
        ctx.lineWidth(lineWidth);
};
const setStrokeColor = (ctx, node) => {
    if (!node.props)
        return;
    if (!('stroke' in node.props))
        return;
    const strokeColor = node.props.stroke;
    if (strokeColor)
        ctx.strokeColor(strokeColor);
};
const setOpacity = (ctx, node) => {
    if (!node.props)
        return;
    if (!('opacity' in node.props))
        return;
    const opacity = node.props.opacity;
    if (!isNil(opacity))
        ctx.opacity(opacity);
};
const setFillOpacity = (ctx, node) => {
    if (!node.props)
        return;
    if (!('fillOpacity' in node.props))
        return;
    const fillOpacity = node.props.fillOpacity || null;
    if (!isNil(fillOpacity))
        ctx.fillOpacity(fillOpacity);
};
const setStrokeOpacity = (ctx, node) => {
    if (!node.props)
        return;
    if (!('strokeOpacity' in node.props))
        return;
    const strokeOpacity = node.props?.strokeOpacity;
    if (!isNil(strokeOpacity))
        ctx.strokeOpacity(strokeOpacity);
};
const setLineJoin = (ctx, node) => {
    if (!node.props)
        return;
    if (!('strokeLinejoin' in node.props))
        return;
    const lineJoin = node.props.strokeLinejoin;
    if (lineJoin)
        ctx.lineJoin(lineJoin);
};
const setLineCap = (ctx, node) => {
    if (!node.props)
        return;
    if (!('strokeLinecap' in node.props))
        return;
    const lineCap = node.props?.strokeLinecap;
    if (lineCap)
        ctx.lineCap(lineCap);
};
const setLineDash = (ctx, node) => {
    if (!node.props)
        return;
    if (!('strokeDasharray' in node.props))
        return;
    const value = node.props?.strokeDasharray || null;
    // @ts-expect-error check this works as expected
    if (value)
        ctx.dash(value.split(/[\s,]+/).map(Number));
};
const hasLinearGradientFill = (node) => {
    if (!node.props)
        return false;
    if (!('fill' in node.props))
        return false;
    if (typeof node.props.fill === 'string')
        return false;
    return node.props.fill?.type === P.LinearGradient;
};
const hasRadialGradientFill = (node) => {
    if (!node.props)
        return false;
    if (!('fill' in node.props))
        return false;
    if (typeof node.props.fill === 'string')
        return false;
    return node.props.fill?.type === P.RadialGradient;
};
function multiplyMatrices(m1, m2) {
    const a = m1[0] * m2[0] + m1[2] * m2[1];
    const b = m1[1] * m2[0] + m1[3] * m2[1];
    const c = m1[0] * m2[2] + m1[2] * m2[3];
    const d = m1[1] * m2[2] + m1[3] * m2[3];
    const e = m1[0] * m2[4] + m1[2] * m2[5] + m1[4];
    const f = m1[1] * m2[4] + m1[3] * m2[5] + m1[5];
    return [a, b, c, d, e, f];
}
const transformGradient = (grad, transforms, bbox, units) => {
    const matrices = transforms.map((transform) => {
        switch (transform.operation) {
            case 'scale': {
                const value = transform.value;
                return [value[0], 0, 0, value[1], 0, 0];
            }
            case 'translate': {
                const value = transform.value;
                let x = value[0] || 0;
                let y = value[1] || 0;
                if (units === 'objectBoundingBox') {
                    x = (bbox[2] - bbox[0]) * x;
                    y = (bbox[3] - bbox[1]) * y;
                }
                return [1, 0, 0, 1, x, y];
            }
            case 'rotate': {
                const value = transform.value;
                const cos = Math.cos(value[0]);
                const sin = Math.sin(value[0]);
                return [cos, sin, -sin, cos, 0, 0];
            }
            case 'skew': {
                const value = transform.value;
                return [1, Math.tan(value[0]), Math.tan(value[1]), 1, 0, 0];
            }
            case 'matrix': {
                const value = transform.value;
                let x = value[4] || 0;
                let y = value[5] || 0;
                if (units === 'objectBoundingBox') {
                    x = (bbox[2] - bbox[0]) * x;
                    y = (bbox[3] - bbox[1]) * y;
                }
                return [value[0], value[1], value[2], value[3], x, y];
            }
            default:
                return [1, 0, 0, 1, 0, 0];
        }
    });
    const matrix = matrices.reduce(multiplyMatrices, [1, 0, 0, 1, 0, 0]);
    grad.setTransform(...matrix);
};
// Math simplified from https://github.com/devongovett/svgkit/blob/master/src/elements/SVGGradient.js#L104
const setLinearGradientFill = (ctx, node) => {
    if (!node.props)
        return;
    if (!('fill' in node.props))
        return;
    const bbox = getBoundingBox(node);
    const gradient = node.props?.fill;
    if (!gradient)
        return;
    const units = gradient.props.gradientUnits || 'objectBoundingBox';
    const transforms = gradient.props.gradientTransform || [];
    let x1 = gradient.props.x1 || 0;
    let y1 = gradient.props.y1 || 0;
    let x2 = gradient.props.x2 || 1;
    let y2 = gradient.props.y2 || 0;
    if (units === 'objectBoundingBox') {
        const m0 = bbox[2] - bbox[0];
        const m3 = bbox[3] - bbox[1];
        const m4 = bbox[0];
        const m5 = bbox[1];
        x1 = m0 * x1 + m4;
        y1 = m3 * y1 + m5;
        x2 = m0 * x2 + m4;
        y2 = m3 * y2 + m5;
    }
    const grad = ctx.linearGradient(x1, y1, x2, y2);
    transformGradient(grad, transforms, bbox, units);
    gradient.children?.forEach((stop) => {
        grad.stop(stop.props.offset, stop.props.stopColor, stop.props.stopOpacity);
    });
    ctx.fill(grad);
};
// Math simplified from https://github.com/devongovett/svgkit/blob/master/src/elements/SVGGradient.js#L155
const setRadialGradientFill = (ctx, node) => {
    if (!node.props)
        return;
    if (!('fill' in node.props))
        return;
    const bbox = getBoundingBox(node);
    const gradient = node.props?.fill;
    if (!gradient)
        return;
    const units = gradient.props.gradientUnits || 'objectBoundingBox';
    const transforms = gradient.props.gradientTransform || [];
    let r = gradient.props.r || 0.5;
    let cx = gradient.props.cx || 0.5;
    let cy = gradient.props.cy || 0.5;
    let fx = gradient.props.fx || cx;
    let fy = gradient.props.fy || cy;
    if (units === 'objectBoundingBox') {
        const m0 = bbox[2] - bbox[0];
        const m3 = bbox[3] - bbox[1];
        const m4 = bbox[0];
        const m5 = bbox[1];
        r = r * m0;
        cx = m0 * cx + m4;
        cy = m3 * cy + m5;
        fx = m0 * fx + m4;
        fy = m3 * fy + m5;
    }
    const grad = ctx.radialGradient(cx, cy, 0, fx, fy, r);
    transformGradient(grad, transforms, bbox, units);
    gradient.children?.forEach((stop) => {
        grad.stop(stop.props.offset, stop.props.stopColor, stop.props.stopOpacity);
    });
    ctx.fill(grad);
};
const setFillColor = (ctx, node) => {
    if (!node.props)
        return;
    if (!('fill' in node.props))
        return;
    const fillColor = node.props?.fill;
    if (fillColor)
        ctx.fillColor(fillColor);
};
const setFill = (ctx, node) => {
    if (hasLinearGradientFill(node))
        return setLinearGradientFill(ctx, node);
    if (hasRadialGradientFill(node))
        return setRadialGradientFill(ctx, node);
    return setFillColor(ctx, node);
};
const draw = (ctx, node) => {
    const props = node.props || {};
    if ('fill' in props && 'stroke' in props && props.fill && props.stroke) {
        ctx.fillAndStroke(props.fillRule);
    }
    else if ('fill' in props && props.fill) {
        ctx.fill(props.fillRule);
    }
    else if ('stroke' in props && props.stroke) {
        ctx.stroke();
    }
    else {
        ctx.save();
        ctx.opacity(0);
        ctx.fill(null);
        ctx.restore();
    }
};
const noop = () => { };
const renderFns$1 = {
    [P.Tspan]: noop,
    [P.TextInstance]: noop,
    [P.Path]: renderPath,
    [P.Rect]: renderRect,
    [P.Line]: renderLine$1,
    [P.G]: renderGroup,
    [P.Text]: renderSvgText,
    [P.Circle]: renderCircle,
    [P.Image]: renderImage$1,
    [P.Ellipse]: renderEllipse,
    [P.Polygon]: renderPolygon,
    [P.Polyline]: renderPolyline,
};
const renderNode$1 = (ctx, node) => {
    const renderFn = renderFns$1[node.type];
    if (renderFn) {
        renderFn(ctx, node);
    }
    else {
        console.warn(`SVG node of type ${node.type} is not currently supported`);
    }
};
const drawNode = (ctx, node) => {
    setLineCap(ctx, node);
    setLineDash(ctx, node);
    setLineJoin(ctx, node);
    setStrokeWidth(ctx, node);
    setStrokeColor(ctx, node);
    setFill(ctx, node);
    setStrokeOpacity(ctx, node);
    setFillOpacity(ctx, node);
    setOpacity(ctx, node);
    applyTransformations(ctx, node);
    renderNode$1(ctx, node);
    draw(ctx, node);
};
const clipPath = (ctx, node) => {
    if (!node.props)
        return;
    if (!('clipPath' in node.props))
        return;
    const value = node.props.clipPath;
    if (value) {
        const children = value.children || [];
        children.forEach((child) => renderNode$1(ctx, child));
        ctx.clip();
    }
};
const drawChildren = (ctx, node) => {
    const children = node.children || [];
    children.forEach((child) => {
        ctx.save();
        clipPath(ctx, child);
        drawNode(ctx, child);
        drawChildren(ctx, child);
        ctx.restore();
    });
};
const resolveAspectRatio = (ctx, node) => {
    if (!node.box)
        return;
    const { width, height } = node.box;
    const { viewBox, preserveAspectRatio } = node.props;
    const { meetOrSlice = 'meet', align = 'xMidYMid' } = preserveAspectRatio || {};
    if (viewBox == null || width == null || height == null)
        return;
    const x = viewBox?.minX || 0;
    const y = viewBox?.minY || 0;
    const logicalWidth = viewBox?.maxX || width;
    const logicalHeight = viewBox?.maxY || height;
    const logicalRatio = logicalWidth / logicalHeight;
    const physicalRatio = width / height;
    const scaleX = width / logicalWidth;
    const scaleY = height / logicalHeight;
    if (align === 'none') {
        ctx.scale(scaleX, scaleY);
        ctx.translate(-x, -y);
        return;
    }
    if ((logicalRatio < physicalRatio && meetOrSlice === 'meet') ||
        (logicalRatio >= physicalRatio && meetOrSlice === 'slice')) {
        ctx.scale(scaleY, scaleY);
        switch (align) {
            case 'xMinYMin':
            case 'xMinYMid':
            case 'xMinYMax':
                ctx.translate(-x, -y);
                break;
            case 'xMidYMin':
            case 'xMidYMid':
            case 'xMidYMax':
                ctx.translate(-x - (logicalWidth - (width * logicalHeight) / height) / 2, -y);
                break;
            default:
                ctx.translate(-x - (logicalWidth - (width * logicalHeight) / height), -y);
        }
    }
    else {
        ctx.scale(scaleX, scaleX);
        switch (align) {
            case 'xMinYMin':
            case 'xMidYMin':
            case 'xMaxYMin':
                ctx.translate(-x, -y);
                break;
            case 'xMinYMid':
            case 'xMidYMid':
            case 'xMaxYMid':
                ctx.translate(-x, -y - (logicalHeight - (height * logicalWidth) / width) / 2);
                break;
            default:
                ctx.translate(-x, -y - (logicalHeight - (height * logicalWidth) / width));
        }
    }
};
const moveToOrigin = (ctx, node) => {
    if (!node.box)
        return;
    const { top, left } = node.box;
    const paddingLeft = node.box.paddingLeft || 0;
    const paddingTop = node.box.paddingTop || 0;
    ctx.translate(left + paddingLeft, top + paddingTop);
};
const renderSvg = (ctx, node) => {
    ctx.save();
    clipNode(ctx, node);
    moveToOrigin(ctx, node);
    resolveAspectRatio(ctx, node);
    drawChildren(ctx, node);
    ctx.restore();
};

const black = { value: '#000', opacity: 1 };
// TODO: parse to number[] in layout to avoid this step
const parseColor = (hex) => {
    if (!hex)
        return black;
    const parsed = colorString.get(hex);
    if (!parsed)
        return black;
    const value = colorString.to.hex(parsed.value.slice(0, 3));
    const opacity = parsed.value[3];
    return { value, opacity };
};

const DEST_REGEXP = /^#.+/;
const isSrcId$1 = (src) => src.match(DEST_REGEXP);
const renderAttachment = (ctx, attachment) => {
    const { xOffset = 0, yOffset = 0, width, height, image } = attachment;
    ctx.translate(-width + xOffset, -height + yOffset);
    ctx.image(image, 0, 0, {
        fit: [width, height],
        align: 'center',
        valign: 'bottom',
    });
};
const renderAttachments = (ctx, run, glyphs) => {
    if (!glyphs)
        return;
    if (!run.positions)
        return;
    const font = run.attributes.font?.[0];
    if (!font)
        return;
    ctx.save();
    const space = font.glyphForCodePoint(0x20);
    const objectReplacement = font.glyphForCodePoint(0xfffc);
    let attachmentAdvance = 0;
    for (let i = 0; i < glyphs.length; i += 1) {
        const position = run.positions[i];
        const glyph = glyphs[i];
        attachmentAdvance += position.xAdvance || 0;
        if (glyph.id === objectReplacement.id && run.attributes.attachment) {
            ctx.translate(attachmentAdvance, position.yOffset || 0);
            renderAttachment(ctx, run.attributes.attachment);
            glyphs[i] = space;
            attachmentAdvance = 0;
        }
    }
    ctx.restore();
};
const renderRun = (ctx, run) => {
    if (!run.glyphs)
        return;
    if (!run.positions)
        return;
    const font = run.attributes.font?.[0];
    if (!font)
        return;
    const { fontSize, link } = run.attributes;
    const color = parseColor(run.attributes.color);
    const opacity = isNil(run.attributes.opacity)
        ? color.opacity
        : run.attributes.opacity;
    const { height = 0, descent = 0, xAdvance = 0 } = run;
    ctx.fillColor(color.value);
    ctx.fillOpacity(opacity);
    if (link) {
        if (isSrcId$1(link)) {
            ctx.goTo(0, -height - descent, xAdvance, height, link.slice(1));
        }
        else {
            ctx.link(0, -height - descent, xAdvance, height, link);
        }
    }
    // Copy glyphs to avoid mutating the original array
    const glyphs = [...run.glyphs];
    renderAttachments(ctx, run, glyphs);
    ctx.font(font.type === 'STANDARD' ? font.fullName : font, fontSize);
    try {
        renderGlyphs(ctx, glyphs, run.positions, 0, 0);
    }
    catch (error) {
        console.log(error);
    }
    ctx.translate(xAdvance, 0);
};
const renderBackground$1 = (ctx, rect, backgroundColor) => {
    const color = parseColor(backgroundColor);
    ctx.save();
    ctx.fillOpacity(color.opacity);
    ctx.rect(rect.x, rect.y, rect.width, rect.height);
    ctx.fill(color.value);
    ctx.restore();
};
const renderDecorationLine = (ctx, decorationLine) => {
    ctx.save();
    ctx.lineWidth(decorationLine.rect.height);
    ctx.strokeOpacity(decorationLine.opacity);
    if (/dashed/.test(decorationLine.style)) {
        ctx.dash(3 * decorationLine.rect.height, {});
    }
    else if (/dotted/.test(decorationLine.style)) {
        ctx.dash(decorationLine.rect.height, {});
    }
    if (/wavy/.test(decorationLine.style)) {
        const dist = Math.max(2, decorationLine.rect.height);
        let step = 1.1 * dist;
        const stepCount = Math.floor(decorationLine.rect.width / (2 * step));
        // Adjust step to fill entire width
        const remainingWidth = decorationLine.rect.width - stepCount * 2 * step;
        const adjustment = remainingWidth / stepCount / 2;
        step += adjustment;
        const cp1y = decorationLine.rect.y + dist;
        const cp2y = decorationLine.rect.y - dist;
        let { x } = decorationLine.rect;
        ctx.moveTo(decorationLine.rect.x, decorationLine.rect.y);
        for (let i = 0; i < stepCount; i += 1) {
            ctx.bezierCurveTo(x + step, cp1y, x + step, cp2y, x + 2 * step, decorationLine.rect.y);
            x += 2 * step;
        }
    }
    else {
        ctx.moveTo(decorationLine.rect.x, decorationLine.rect.y);
        ctx.lineTo(decorationLine.rect.x + decorationLine.rect.width, decorationLine.rect.y);
        if (/double/.test(decorationLine.style)) {
            ctx.moveTo(decorationLine.rect.x, decorationLine.rect.y + decorationLine.rect.height * 2);
            ctx.lineTo(decorationLine.rect.x + decorationLine.rect.width, decorationLine.rect.y + decorationLine.rect.height * 2);
        }
    }
    ctx.stroke(decorationLine.color);
    ctx.restore();
};
const renderLine = (ctx, line) => {
    if (!line.box)
        return;
    const lineAscent = line.ascent || 0;
    ctx.save();
    ctx.translate(line.box.x, line.box.y + lineAscent);
    for (let i = 0; i < line.runs.length; i += 1) {
        const run = line.runs[i];
        const isLastRun = i === line.runs.length - 1;
        if (run.attributes.backgroundColor) {
            const xAdvance = run.xAdvance ?? 0;
            const overflowRight = isLastRun ? line.overflowRight ?? 0 : 0;
            const backgroundRect = {
                x: 0,
                y: -lineAscent,
                height: line.box.height,
                width: xAdvance - overflowRight,
            };
            renderBackground$1(ctx, backgroundRect, run.attributes.backgroundColor);
        }
        renderRun(ctx, run);
    }
    ctx.restore();
    ctx.save();
    ctx.translate(line.box.x, line.box.y);
    if (line.decorationLines) {
        for (let i = 0; i < line.decorationLines.length; i += 1) {
            const decorationLine = line.decorationLines[i];
            renderDecorationLine(ctx, decorationLine);
        }
    }
    ctx.restore();
};
const renderBlock = (ctx, block) => {
    block.forEach((line) => {
        renderLine(ctx, line);
    });
};
const renderText = (ctx, node) => {
    if (!node.box)
        return;
    if (!node.lines)
        return;
    const { top, left } = node.box;
    const blocks = [node.lines];
    const paddingTop = node.box?.paddingTop || 0;
    const paddingLeft = node.box?.paddingLeft || 0;
    const initialY = node.lines[0] ? node.lines[0].box.y : 0;
    const offsetX = node.alignOffset || 0;
    ctx.save();
    ctx.translate(left + paddingLeft - offsetX, top + paddingTop - initialY);
    blocks.forEach((block) => {
        renderBlock(ctx, block);
    });
    ctx.restore();
};

const renderPage = (ctx, node) => {
    if (!node.box)
        return;
    const { width, height } = node.box;
    const dpi = node.props?.dpi || 72;
    const userUnit = dpi / 72;
    ctx.addPage({ size: [width, height], margin: 0, userUnit });
};

const renderNote = (ctx, node) => {
    if (!node.box)
        return;
    const { top, left } = node.box;
    const value = node?.children?.[0].value || '';
    const color = node.style?.backgroundColor;
    ctx.note(left, top, 0, 0, value, { color });
};

const embedImage = (ctx, node) => {
    const src = node.image.data;
    let image;
    if (typeof src === 'string') {
        image = ctx._imageRegistry[src];
    }
    if (!image) {
        image = ctx.openImage(src);
    }
    if (!image.obj) {
        image.embed(ctx);
    }
    return image;
};

const isNumeric = (n) => {
    return !Number.isNaN(parseFloat(n)) && Number.isFinite(n);
};
const applyContainObjectFit = (cw, ch, iw, ih, px, py) => {
    const cr = cw / ch;
    const ir = iw / ih;
    const pxp = matchPercent(px ?? null);
    const pyp = matchPercent(py ?? null);
    const pxv = pxp ? pxp.percent : 0.5;
    const pyv = pyp ? pyp.percent : 0.5;
    if (cr > ir) {
        const height = ch;
        const width = height * ir;
        const yOffset = isNumeric(py) ? py : 0;
        const xOffset = isNumeric(px) ? px : (cw - width) * pxv;
        return { width, height, xOffset, yOffset };
    }
    const width = cw;
    const height = width / ir;
    const xOffset = isNumeric(px) ? px : 0;
    const yOffset = isNumeric(py) ? py : (ch - height) * pyv;
    return { width, height, yOffset, xOffset };
};
const applyNoneObjectFit = (cw, ch, iw, ih, px, py) => {
    const width = iw;
    const height = ih;
    const pxp = matchPercent(px ?? null);
    const pyp = matchPercent(py ?? null);
    const pxv = pxp ? pxp.percent : 0.5;
    const pyv = pyp ? pyp.percent : 0.5;
    const xOffset = isNumeric(px) ? px : (cw - width) * pxv;
    const yOffset = isNumeric(py) ? py : (ch - height) * pyv;
    return { width, height, xOffset, yOffset };
};
const applyCoverObjectFit = (cw, ch, iw, ih, px, py) => {
    const ir = iw / ih;
    const cr = cw / ch;
    const pxp = matchPercent(px ?? null);
    const pyp = matchPercent(py ?? null);
    const pxv = pxp ? pxp.percent : 0.5;
    const pyv = pyp ? pyp.percent : 0.5;
    if (cr > ir) {
        const width = cw;
        const height = width / ir;
        const xOffset = isNumeric(px) ? px : 0;
        const yOffset = isNumeric(py) ? py : (ch - height) * pyv;
        return { width, height, yOffset, xOffset };
    }
    const height = ch;
    const width = height * ir;
    const xOffset = isNumeric(px) ? px : (cw - width) * pxv;
    const yOffset = isNumeric(py) ? py : 0;
    return { width, height, xOffset, yOffset };
};
const applyScaleDownObjectFit = (cw, ch, iw, ih, px, py) => {
    const containDimension = applyContainObjectFit(cw, ch, iw, ih, px, py);
    const noneDimension = applyNoneObjectFit(cw, ch, iw, ih, px, py);
    return containDimension.width < noneDimension.width
        ? containDimension
        : noneDimension;
};
const applyFillObjectFit = (cw, ch, px, py) => {
    return {
        width: cw,
        height: ch,
        xOffset: matchPercent(px ?? null) ? 0 : px || 0,
        yOffset: matchPercent(py ?? null) ? 0 : py || 0,
    };
};
const resolveObjectFit = (type = 'fill', cw, ch, iw, ih, px, py) => {
    switch (type) {
        case 'contain':
            return applyContainObjectFit(cw, ch, iw, ih, px, py);
        case 'cover':
            return applyCoverObjectFit(cw, ch, iw, ih, px, py);
        case 'none':
            return applyNoneObjectFit(cw, ch, iw, ih, px, py);
        case 'scale-down':
            return applyScaleDownObjectFit(cw, ch, iw, ih, px, py);
        default:
            return applyFillObjectFit(cw, ch, px, py);
    }
};

const drawImage = (ctx, node, options) => {
    if (!node.box)
        return;
    if (!node.image)
        return;
    const { left, top } = node.box;
    const opacity = node.style?.opacity;
    const objectFit = node.style?.objectFit;
    const objectPositionX = node.style?.objectPositionX;
    const objectPositionY = node.style?.objectPositionY;
    const paddingTop = node.box.paddingTop || 0;
    const paddingRight = node.box.paddingRight || 0;
    const paddingBottom = node.box.paddingBottom || 0;
    const paddingLeft = node.box.paddingLeft || 0;
    const imageCache = options.imageCache || new Map();
    const { width, height, xOffset, yOffset } = resolveObjectFit(objectFit, node.box.width - paddingLeft - paddingRight, node.box.height - paddingTop - paddingBottom, node.image.width, node.image.height, objectPositionX, objectPositionY);
    if (node.image.data) {
        if (width !== 0 && height !== 0) {
            const cacheKey = node.image.key;
            const image = imageCache.get(cacheKey) || embedImage(ctx, node);
            if (cacheKey)
                imageCache.set(cacheKey, image);
            const imageOpacity = isNil(opacity) ? 1 : opacity;
            ctx
                .fillOpacity(imageOpacity)
                .image(image, left + paddingLeft + xOffset, top + paddingTop + yOffset, {
                width,
                height,
            });
        }
        else {
            console.warn(`Image with src '${JSON.stringify(node.props.src || node.props.source)}' skipped due to invalid dimensions`);
        }
    }
};
const renderImage = (ctx, node, options) => {
    ctx.save();
    clipNode(ctx, node);
    drawImage(ctx, node, options);
    ctx.restore();
};

const CONTENT_COLOR = '#a1c6e7';
const PADDING_COLOR = '#c4deb9';
const MARGIN_COLOR = '#f8cca1';
// TODO: Draw debug boxes using clipping to enhance quality
const debugContent = (ctx, node) => {
    if (!node.box)
        return;
    const { left, top, width, height, paddingLeft = 0, paddingTop = 0, paddingRight = 0, paddingBottom = 0, borderLeftWidth = 0, borderTopWidth = 0, borderRightWidth = 0, borderBottomWidth = 0, } = node.box;
    ctx
        .fillColor(CONTENT_COLOR)
        .opacity(0.5)
        .rect(left + paddingLeft + borderLeftWidth, top + paddingTop + borderTopWidth, width - paddingLeft - paddingRight - borderRightWidth - borderLeftWidth, height - paddingTop - paddingBottom - borderTopWidth - borderBottomWidth)
        .fill();
};
const debugPadding = (ctx, node) => {
    if (!node.box)
        return;
    const { left, top, width, height, paddingLeft = 0, paddingTop = 0, paddingRight = 0, paddingBottom = 0, borderLeftWidth = 0, borderTopWidth = 0, borderRightWidth = 0, borderBottomWidth = 0, } = node.box;
    ctx.fillColor(PADDING_COLOR).opacity(0.5);
    // Padding top
    ctx
        .rect(left + paddingLeft + borderLeftWidth, top + borderTopWidth, width - paddingRight - paddingLeft - borderLeftWidth - borderRightWidth, paddingTop)
        .fill();
    // Padding left
    ctx
        .rect(left + borderLeftWidth, top + borderTopWidth, paddingLeft, height - borderTopWidth - borderBottomWidth)
        .fill();
    // Padding right
    ctx
        .rect(left + width - paddingRight - borderRightWidth, top + borderTopWidth, paddingRight, height - borderTopWidth - borderBottomWidth)
        .fill();
    // Padding bottom
    ctx
        .rect(left + paddingLeft + borderLeftWidth, top + height - paddingBottom - borderBottomWidth, width - paddingRight - paddingLeft - borderLeftWidth - borderRightWidth, paddingBottom)
        .fill();
};
const debugMargin = (ctx, node) => {
    if (!node.box)
        return;
    const { left, top, width, height } = node.box;
    const { marginLeft = 0, marginTop = 0, marginRight = 0, marginBottom = 0, } = node.box;
    ctx.fillColor(MARGIN_COLOR).opacity(0.5);
    // Margin top
    ctx.rect(left, top - marginTop, width, marginTop).fill();
    // Margin left
    ctx
        .rect(left - marginLeft, top - marginTop, marginLeft, height + marginTop + marginBottom)
        .fill();
    // Margin right
    ctx
        .rect(left + width, top - marginTop, marginRight, height + marginTop + marginBottom)
        .fill();
    // Margin bottom
    ctx.rect(left, top + height, width, marginBottom).fill();
};
const debugText = (ctx, node) => {
    if (!node.box)
        return;
    const { left, top, width, height } = node.box;
    const { marginLeft = 0, marginTop = 0, marginRight = 0, marginBottom = 0, } = node.box;
    const roundedWidth = Math.round(width + marginLeft + marginRight);
    const roundedHeight = Math.round(height + marginTop + marginBottom);
    ctx
        .fontSize(6)
        .opacity(1)
        .fillColor('black')
        .text(`${roundedWidth} x ${roundedHeight}`, left - marginLeft, Math.max(top - marginTop - 4, 1), { width: Infinity });
};
const debugOrigin = (ctx, node) => {
    if (node.origin) {
        ctx
            .circle(node.origin.left, node.origin.top, 3)
            .fill('red')
            .circle(node.origin.left, node.origin.top, 5)
            .stroke('red');
    }
};
const renderDebug = (ctx, node) => {
    if (!node.props)
        return;
    if (!('debug' in node.props) || !node.props.debug)
        return;
    ctx.save();
    debugContent(ctx, node);
    debugPadding(ctx, node);
    debugMargin(ctx, node);
    debugText(ctx, node);
    debugOrigin(ctx, node);
    ctx.restore();
};

const availableMethods = [
    'dash',
    'clip',
    'save',
    'path',
    'fill',
    'font',
    'text',
    'rect',
    'scale',
    'moveTo',
    'lineTo',
    'stroke',
    'rotate',
    'circle',
    'lineCap',
    'opacity',
    'ellipse',
    'polygon',
    'restore',
    'lineJoin',
    'fontSize',
    'fillColor',
    'lineWidth',
    'translate',
    'miterLimit',
    'strokeColor',
    'fillOpacity',
    'roundedRect',
    'fillAndStroke',
    'strokeOpacity',
    'bezierCurveTo',
    'quadraticCurveTo',
    'linearGradient',
    'radialGradient',
];
const painter = (ctx) => {
    const p = availableMethods.reduce((acc, prop) => ({
        ...acc,
        [prop]: (...args) => {
            // @ts-expect-error ctx[prop] is a function
            ctx[prop](...args);
            return p;
        },
    }), {});
    return p;
};
const renderCanvas = (ctx, node) => {
    if (!node.box)
        return;
    const { top, left, width, height } = node.box;
    const paddingTop = node.box.paddingTop || 0;
    const paddingLeft = node.box.paddingLeft || 0;
    const paddingRight = node.box.paddingRight || 0;
    const paddingBottom = node.box.paddingBottom || 0;
    const availableWidth = width - paddingLeft - paddingRight;
    const availableHeight = height - paddingTop - paddingBottom;
    if (!availableWidth || !availableHeight) {
        console.warn('Canvas element has null width or height. Please provide valid values via the `style` prop in order to correctly render it.');
    }
    ctx.save().translate(left + paddingLeft, top + paddingTop);
    if (node.props.paint) {
        node.props.paint(painter(ctx), availableWidth, availableHeight);
    }
    ctx.restore();
};

// Ref: https://www.w3.org/TR/css-backgrounds-3/#borders
// This constant is used to approximate a symmetrical arc using a cubic Bezier curve.
const KAPPA = 4.0 * ((Math.sqrt(2) - 1.0) / 3.0);
const clipBorderTop = (ctx, layout, style, rtr, rtl) => {
    const { top, left, width, height } = layout;
    const { borderTopWidth, borderRightWidth, borderLeftWidth } = style;
    // Clip outer top border edge
    ctx.moveTo(left + rtl, top);
    ctx.lineTo(left + width - rtr, top);
    // Ellipse coefficients outer top right cap
    const c0 = rtr * (1.0 - KAPPA);
    // Clip outer top right cap
    ctx.bezierCurveTo(left + width - c0, top, left + width, top + c0, left + width, top + rtr);
    // Move down in case the margin exceedes the radius
    const topRightYCoord = top + Math.max(borderTopWidth, rtr);
    ctx.lineTo(left + width, topRightYCoord);
    // Clip inner top right cap
    ctx.lineTo(left + width - borderRightWidth, topRightYCoord);
    // Ellipse coefficients inner top right cap
    const innerTopRightRadiusX = Math.max(rtr - borderRightWidth, 0);
    const innerTopRightRadiusY = Math.max(rtr - borderTopWidth, 0);
    const c1 = innerTopRightRadiusX * (1.0 - KAPPA);
    const c2 = innerTopRightRadiusY * (1.0 - KAPPA);
    // Clip inner top right cap
    ctx.bezierCurveTo(left + width - borderRightWidth, top + borderTopWidth + c2, left + width - borderRightWidth - c1, top + borderTopWidth, left + width - borderRightWidth - innerTopRightRadiusX, top + borderTopWidth);
    // Clip inner top border edge
    ctx.lineTo(left + Math.max(rtl, borderLeftWidth), top + borderTopWidth);
    // Ellipse coefficients inner top left cap
    const innerTopLeftRadiusX = Math.max(rtl - borderLeftWidth, 0);
    const innerTopLeftRadiusY = Math.max(rtl - borderTopWidth, 0);
    const c3 = innerTopLeftRadiusX * (1.0 - KAPPA);
    const c4 = innerTopLeftRadiusY * (1.0 - KAPPA);
    const topLeftYCoord = top + Math.max(borderTopWidth, rtl);
    // Clip inner top left cap
    ctx.bezierCurveTo(left + borderLeftWidth + c3, top + borderTopWidth, left + borderLeftWidth, top + borderTopWidth + c4, left + borderLeftWidth, topLeftYCoord);
    ctx.lineTo(left, topLeftYCoord);
    // Move down in case the margin exceedes the radius
    ctx.lineTo(left, top + rtl);
    // Ellipse coefficients outer top left cap
    const c5 = rtl * (1.0 - KAPPA);
    // Clip outer top left cap
    ctx.bezierCurveTo(left, top + c5, left + c5, top, left + rtl, top);
    ctx.closePath();
    ctx.clip();
    // Clip border top cap joins
    if (borderRightWidth) {
        const trSlope = -borderTopWidth / borderRightWidth;
        ctx.moveTo(left + width / 2, trSlope * (-width / 2) + top);
        ctx.lineTo(left + width, top);
        ctx.lineTo(left, top);
        ctx.lineTo(left, top + height);
        ctx.closePath();
        ctx.clip();
    }
    if (borderLeftWidth) {
        const trSlope = -borderTopWidth / borderLeftWidth;
        ctx.moveTo(left + width / 2, trSlope * (-width / 2) + top);
        ctx.lineTo(left, top);
        ctx.lineTo(left + width, top);
        ctx.lineTo(left + width, top + height);
        ctx.closePath();
        ctx.clip();
    }
};
const fillBorderTop = (ctx, layout, style, rtr, rtl) => {
    const { top, left, width } = layout;
    const { borderTopColor, borderTopWidth, borderTopStyle, borderRightWidth, borderLeftWidth, } = style;
    const c0 = rtl * (1.0 - KAPPA);
    const c1 = rtr * (1.0 - KAPPA);
    ctx.moveTo(left, top + Math.max(rtl, borderTopWidth));
    ctx.bezierCurveTo(left, top + c0, left + c0, top, left + rtl, top);
    ctx.lineTo(left + width - rtr, top);
    ctx.bezierCurveTo(left + width - c1, top, left + width, top + c1, left + width, top + rtr);
    ctx.strokeColor(borderTopColor);
    ctx.lineWidth(Math.max(borderRightWidth, borderTopWidth, borderLeftWidth) * 2);
    if (borderTopStyle === 'dashed') {
        ctx.dash(borderTopWidth * 2, { space: borderTopWidth * 1.2 });
    }
    else if (borderTopStyle === 'dotted') {
        ctx.dash(borderTopWidth, { space: borderTopWidth * 1.2 });
    }
    ctx.stroke();
    ctx.undash();
};
const clipBorderRight = (ctx, layout, style, rtr, rbr) => {
    const { top, left, width, height } = layout;
    const { borderTopWidth, borderRightWidth, borderBottomWidth } = style;
    // Clip outer right border edge
    ctx.moveTo(left + width, top + rtr);
    ctx.lineTo(left + width, top + height - rbr);
    // Ellipse coefficients outer bottom right cap
    const c0 = rbr * (1.0 - KAPPA);
    // Clip outer top right cap
    ctx.bezierCurveTo(left + width, top + height - c0, left + width - c0, top + height, left + width - rbr, top + height);
    // Move left in case the margin exceedes the radius
    const topBottomXCoord = left + width - Math.max(borderRightWidth, rbr);
    ctx.lineTo(topBottomXCoord, top + height);
    // Clip inner bottom right cap
    ctx.lineTo(topBottomXCoord, top + height - borderBottomWidth);
    // Ellipse coefficients inner bottom right cap
    const innerBottomRightRadiusX = Math.max(rbr - borderRightWidth, 0);
    const innerBottomRightRadiusY = Math.max(rbr - borderBottomWidth, 0);
    const c1 = innerBottomRightRadiusX * (1.0 - KAPPA);
    const c2 = innerBottomRightRadiusY * (1.0 - KAPPA);
    // Clip inner top right cap
    ctx.bezierCurveTo(left + width - borderRightWidth - c1, top + height - borderBottomWidth, left + width - borderRightWidth, top + height - borderBottomWidth - c2, left + width - borderRightWidth, top + height - Math.max(rbr, borderBottomWidth));
    // Clip inner right border edge
    ctx.lineTo(left + width - borderRightWidth, top + Math.max(rtr, borderTopWidth));
    // Ellipse coefficients inner top right cap
    const innerTopRightRadiusX = Math.max(rtr - borderRightWidth, 0);
    const innerTopRightRadiusY = Math.max(rtr - borderTopWidth, 0);
    const c3 = innerTopRightRadiusX * (1.0 - KAPPA);
    const c4 = innerTopRightRadiusY * (1.0 - KAPPA);
    const topRightXCoord = left + width - Math.max(rtr, borderRightWidth);
    // Clip inner top left cap
    ctx.bezierCurveTo(left + width - borderRightWidth, top + borderTopWidth + c4, left + width - borderRightWidth - c3, top + borderTopWidth, topRightXCoord, top + borderTopWidth);
    ctx.lineTo(topRightXCoord, top);
    // Move right in case the margin exceedes the radius
    ctx.lineTo(left + width - rtr, top);
    // Ellipse coefficients outer top right cap
    const c5 = rtr * (1.0 - KAPPA);
    // Clip outer top right cap
    ctx.bezierCurveTo(left + width - c5, top, left + width, top + c5, left + width, top + rtr);
    ctx.closePath();
    ctx.clip();
    // Clip border right cap joins
    if (borderTopWidth) {
        const trSlope = -borderTopWidth / borderRightWidth;
        ctx.moveTo(left + width / 2, trSlope * (-width / 2) + top);
        ctx.lineTo(left + width, top);
        ctx.lineTo(left + width, top + height);
        ctx.lineTo(left, top + height);
        ctx.closePath();
        ctx.clip();
    }
    if (borderBottomWidth) {
        const brSlope = borderBottomWidth / borderRightWidth;
        ctx.moveTo(left + width / 2, brSlope * (-width / 2) + top + height);
        ctx.lineTo(left + width, top + height);
        ctx.lineTo(left + width, top);
        ctx.lineTo(left, top);
        ctx.closePath();
        ctx.clip();
    }
};
const fillBorderRight = (ctx, layout, style, rtr, rbr) => {
    const { top, left, width, height } = layout;
    const { borderRightColor, borderRightStyle, borderRightWidth, borderTopWidth, borderBottomWidth, } = style;
    const c0 = rbr * (1.0 - KAPPA);
    const c1 = rtr * (1.0 - KAPPA);
    ctx.moveTo(left + width - rtr, top);
    ctx.bezierCurveTo(left + width - c1, top, left + width, top + c1, left + width, top + rtr);
    ctx.lineTo(left + width, top + height - rbr);
    ctx.bezierCurveTo(left + width, top + height - c0, left + width - c0, top + height, left + width - rbr, top + height);
    ctx.strokeColor(borderRightColor);
    ctx.lineWidth(Math.max(borderRightWidth, borderTopWidth, borderBottomWidth) * 2);
    if (borderRightStyle === 'dashed') {
        ctx.dash(borderRightWidth * 2, { space: borderRightWidth * 1.2 });
    }
    else if (borderRightStyle === 'dotted') {
        ctx.dash(borderRightWidth, { space: borderRightWidth * 1.2 });
    }
    ctx.stroke();
    ctx.undash();
};
const clipBorderBottom = (ctx, layout, style, rbl, rbr) => {
    const { top, left, width, height } = layout;
    const { borderBottomWidth, borderRightWidth, borderLeftWidth } = style;
    // Clip outer top border edge
    ctx.moveTo(left + width - rbr, top + height);
    ctx.lineTo(left + rbl, top + height);
    // Ellipse coefficients outer top right cap
    const c0 = rbl * (1.0 - KAPPA);
    // Clip outer top right cap
    ctx.bezierCurveTo(left + c0, top + height, left, top + height - c0, left, top + height - rbl);
    // Move up in case the margin exceedes the radius
    const bottomLeftYCoord = top + height - Math.max(borderBottomWidth, rbl);
    ctx.lineTo(left, bottomLeftYCoord);
    // Clip inner bottom left cap
    ctx.lineTo(left + borderLeftWidth, bottomLeftYCoord);
    // Ellipse coefficients inner top right cap
    const innerBottomLeftRadiusX = Math.max(rbl - borderLeftWidth, 0);
    const innerBottomLeftRadiusY = Math.max(rbl - borderBottomWidth, 0);
    const c1 = innerBottomLeftRadiusX * (1.0 - KAPPA);
    const c2 = innerBottomLeftRadiusY * (1.0 - KAPPA);
    // Clip inner bottom left cap
    ctx.bezierCurveTo(left + borderLeftWidth, top + height - borderBottomWidth - c2, left + borderLeftWidth + c1, top + height - borderBottomWidth, left + borderLeftWidth + innerBottomLeftRadiusX, top + height - borderBottomWidth);
    // Clip inner bottom border edge
    ctx.lineTo(left + width - Math.max(rbr, borderRightWidth), top + height - borderBottomWidth);
    // Ellipse coefficients inner top left cap
    const innerBottomRightRadiusX = Math.max(rbr - borderRightWidth, 0);
    const innerBottomRightRadiusY = Math.max(rbr - borderBottomWidth, 0);
    const c3 = innerBottomRightRadiusX * (1.0 - KAPPA);
    const c4 = innerBottomRightRadiusY * (1.0 - KAPPA);
    const bottomRightYCoord = top + height - Math.max(borderBottomWidth, rbr);
    // Clip inner top left cap
    ctx.bezierCurveTo(left + width - borderRightWidth - c3, top + height - borderBottomWidth, left + width - borderRightWidth, top + height - borderBottomWidth - c4, left + width - borderRightWidth, bottomRightYCoord);
    ctx.lineTo(left + width, bottomRightYCoord);
    // Move down in case the margin exceedes the radius
    ctx.lineTo(left + width, top + height - rbr);
    // Ellipse coefficients outer top left cap
    const c5 = rbr * (1.0 - KAPPA);
    // Clip outer top left cap
    ctx.bezierCurveTo(left + width, top + height - c5, left + width - c5, top + height, left + width - rbr, top + height);
    ctx.closePath();
    ctx.clip();
    // Clip border bottom cap joins
    if (borderRightWidth) {
        const brSlope = borderBottomWidth / borderRightWidth;
        ctx.moveTo(left + width / 2, brSlope * (-width / 2) + top + height);
        ctx.lineTo(left + width, top + height);
        ctx.lineTo(left, top + height);
        ctx.lineTo(left, top);
        ctx.closePath();
        ctx.clip();
    }
    if (borderLeftWidth) {
        const trSlope = -borderBottomWidth / borderLeftWidth;
        ctx.moveTo(left + width / 2, trSlope * (width / 2) + top + height);
        ctx.lineTo(left, top + height);
        ctx.lineTo(left + width, top + height);
        ctx.lineTo(left + width, top);
        ctx.closePath();
        ctx.clip();
    }
};
const fillBorderBottom = (ctx, layout, style, rbl, rbr) => {
    const { top, left, width, height } = layout;
    const { borderBottomColor, borderBottomStyle, borderBottomWidth, borderRightWidth, borderLeftWidth, } = style;
    const c0 = rbl * (1.0 - KAPPA);
    const c1 = rbr * (1.0 - KAPPA);
    ctx.moveTo(left + width, top + height - rbr);
    ctx.bezierCurveTo(left + width, top + height - c1, left + width - c1, top + height, left + width - rbr, top + height);
    ctx.lineTo(left + rbl, top + height);
    ctx.bezierCurveTo(left + c0, top + height, left, top + height - c0, left, top + height - rbl);
    ctx.strokeColor(borderBottomColor);
    ctx.lineWidth(Math.max(borderBottomWidth, borderRightWidth, borderLeftWidth) * 2);
    if (borderBottomStyle === 'dashed') {
        ctx.dash(borderBottomWidth * 2, { space: borderBottomWidth * 1.2 });
    }
    else if (borderBottomStyle === 'dotted') {
        ctx.dash(borderBottomWidth, { space: borderBottomWidth * 1.2 });
    }
    ctx.stroke();
    ctx.undash();
};
const clipBorderLeft = (ctx, layout, style, rbl, rtl) => {
    const { top, left, width, height } = layout;
    const { borderTopWidth, borderLeftWidth, borderBottomWidth } = style;
    // Clip outer left border edge
    ctx.moveTo(left, top + height - rbl);
    ctx.lineTo(left, top + rtl);
    // Ellipse coefficients outer top left cap
    const c0 = rtl * (1.0 - KAPPA);
    // Clip outer top left cap
    ctx.bezierCurveTo(left, top + c0, left + c0, top, left + rtl, top);
    // Move right in case the margin exceedes the radius
    const topLeftCoordX = left + Math.max(borderLeftWidth, rtl);
    ctx.lineTo(topLeftCoordX, top);
    // Clip inner top left cap
    ctx.lineTo(topLeftCoordX, top + borderTopWidth);
    // Ellipse coefficients inner top left cap
    const innerTopLeftRadiusX = Math.max(rtl - borderLeftWidth, 0);
    const innerTopLeftRadiusY = Math.max(rtl - borderTopWidth, 0);
    const c1 = innerTopLeftRadiusX * (1.0 - KAPPA);
    const c2 = innerTopLeftRadiusY * (1.0 - KAPPA);
    // Clip inner top right cap
    ctx.bezierCurveTo(left + borderLeftWidth + c1, top + borderTopWidth, left + borderLeftWidth, top + borderTopWidth + c2, left + borderLeftWidth, top + Math.max(rtl, borderTopWidth));
    // Clip inner left border edge
    ctx.lineTo(left + borderLeftWidth, top + height - Math.max(rbl, borderBottomWidth));
    // Ellipse coefficients inner bottom left cap
    const innerBottomLeftRadiusX = Math.max(rbl - borderLeftWidth, 0);
    const innerBottomLeftRadiusY = Math.max(rbl - borderBottomWidth, 0);
    const c3 = innerBottomLeftRadiusX * (1.0 - KAPPA);
    const c4 = innerBottomLeftRadiusY * (1.0 - KAPPA);
    const bottomLeftXCoord = left + Math.max(rbl, borderLeftWidth);
    // Clip inner top left cap
    ctx.bezierCurveTo(left + borderLeftWidth, top + height - borderBottomWidth - c4, left + borderLeftWidth + c3, top + height - borderBottomWidth, bottomLeftXCoord, top + height - borderBottomWidth);
    ctx.lineTo(bottomLeftXCoord, top + height);
    // Move left in case the margin exceedes the radius
    ctx.lineTo(left + rbl, top + height);
    // Ellipse coefficients outer top right cap
    const c5 = rbl * (1.0 - KAPPA);
    // Clip outer top right cap
    ctx.bezierCurveTo(left + c5, top + height, left, top + height - c5, left, top + height - rbl);
    ctx.closePath();
    ctx.clip();
    // Clip border right cap joins
    if (borderBottomWidth) {
        const trSlope = -borderBottomWidth / borderLeftWidth;
        ctx.moveTo(left + width / 2, trSlope * (width / 2) + top + height);
        ctx.lineTo(left, top + height);
        ctx.lineTo(left, top);
        ctx.lineTo(left + width, top);
        ctx.closePath();
        ctx.clip();
    }
    if (borderBottomWidth) {
        const trSlope = -borderTopWidth / borderLeftWidth;
        ctx.moveTo(left + width / 2, trSlope * (-width / 2) + top);
        ctx.lineTo(left, top);
        ctx.lineTo(left, top + height);
        ctx.lineTo(left + width, top + height);
        ctx.closePath();
        ctx.clip();
    }
};
const fillBorderLeft = (ctx, layout, style, rbl, rtl) => {
    const { top, left, height } = layout;
    const { borderLeftColor, borderLeftStyle, borderLeftWidth, borderTopWidth, borderBottomWidth, } = style;
    const c0 = rbl * (1.0 - KAPPA);
    const c1 = rtl * (1.0 - KAPPA);
    ctx.moveTo(left + rbl, top + height);
    ctx.bezierCurveTo(left + c0, top + height, left, top + height - c0, left, top + height - rbl);
    ctx.lineTo(left, top + rtl);
    ctx.bezierCurveTo(left, top + c1, left + c1, top, left + rtl, top);
    ctx.strokeColor(borderLeftColor);
    ctx.lineWidth(Math.max(borderLeftWidth, borderTopWidth, borderBottomWidth) * 2);
    if (borderLeftStyle === 'dashed') {
        ctx.dash(borderLeftWidth * 2, { space: borderLeftWidth * 1.2 });
    }
    else if (borderLeftStyle === 'dotted') {
        ctx.dash(borderLeftWidth, { space: borderLeftWidth * 1.2 });
    }
    ctx.stroke();
    ctx.undash();
};
const shouldRenderBorders = (node) => node.box &&
    (node.box.borderTopWidth ||
        node.box.borderRightWidth ||
        node.box.borderBottomWidth ||
        node.box.borderLeftWidth);
const renderBorders = (ctx, node) => {
    if (!node.box)
        return;
    if (!shouldRenderBorders(node))
        return;
    const { width, height, borderTopWidth = 0, borderLeftWidth = 0, borderRightWidth = 0, borderBottomWidth = 0, } = node.box;
    const { opacity = 1, borderTopColor = 'black', borderTopStyle = 'solid', borderLeftColor = 'black', borderLeftStyle = 'solid', borderRightColor = 'black', borderRightStyle = 'solid', borderBottomColor = 'black', borderBottomStyle = 'solid', } = node.style;
    // @ts-expect-error this is always a number due to resolve border radius step
    const borderTopLeftRadius = node.style.borderTopLeftRadius || 0;
    // @ts-expect-error this is always a number due to resolve border radius step
    const borderTopRightRadius = node.style.borderTopRightRadius || 0;
    // @ts-expect-error this is always a number due to resolve border radius step
    const borderBottomLeftRadius = node.style.borderBottomLeftRadius || 0;
    // @ts-expect-error this is always a number due to resolve border radius step
    const borderBottomRightRadius = node.style.borderBottomRightRadius || 0;
    const style = {
        borderTopColor,
        borderTopWidth,
        borderTopStyle,
        borderLeftColor,
        borderLeftWidth,
        borderLeftStyle,
        borderRightColor,
        borderRightWidth,
        borderRightStyle,
        borderBottomColor,
        borderBottomWidth,
        borderBottomStyle};
    const rtr = Math.min(borderTopRightRadius, 0.5 * width, 0.5 * height);
    const rtl = Math.min(borderTopLeftRadius, 0.5 * width, 0.5 * height);
    const rbr = Math.min(borderBottomRightRadius, 0.5 * width, 0.5 * height);
    const rbl = Math.min(borderBottomLeftRadius, 0.5 * width, 0.5 * height);
    ctx.save();
    ctx.strokeOpacity(opacity);
    if (borderTopWidth) {
        ctx.save();
        clipBorderTop(ctx, node.box, style, rtr, rtl);
        fillBorderTop(ctx, node.box, style, rtr, rtl);
        ctx.restore();
    }
    if (borderRightWidth) {
        ctx.save();
        clipBorderRight(ctx, node.box, style, rtr, rbr);
        fillBorderRight(ctx, node.box, style, rtr, rbr);
        ctx.restore();
    }
    if (borderBottomWidth) {
        ctx.save();
        clipBorderBottom(ctx, node.box, style, rbl, rbr);
        fillBorderBottom(ctx, node.box, style, rbl, rbr);
        ctx.restore();
    }
    if (borderLeftWidth) {
        ctx.save();
        clipBorderLeft(ctx, node.box, style, rbl, rtl);
        fillBorderLeft(ctx, node.box, style, rbl, rtl);
        ctx.restore();
    }
    ctx.restore();
};

const drawBackground = (ctx, node) => {
    if (!node.box)
        return;
    const { top, left, width, height } = node.box;
    const color = parseColor(node.style.backgroundColor);
    const nodeOpacity = isNil(node.style?.opacity) ? 1 : node.style.opacity;
    const opacity = Math.min(color.opacity, nodeOpacity);
    ctx
        .fillOpacity(opacity)
        .fillColor(color.value)
        .rect(left, top, width, height)
        .fill();
};
const renderBackground = (ctx, node) => {
    const hasBackground = !!node.box && !!node.style?.backgroundColor;
    if (hasBackground) {
        ctx.save();
        clipNode(ctx, node);
        drawBackground(ctx, node);
        ctx.restore();
    }
};

const isString = (value) => typeof value === 'string';
const isSrcId = (value) => /^#.+/.test(value);
const renderLink = (ctx, node, src) => {
    if (!src || !node.box)
        return;
    const isId = isSrcId(src);
    const method = isId ? 'goTo' : 'link';
    const value = isId ? src.slice(1) : src;
    const { top, left, width, height } = node.box;
    ctx[method](left, top, width, height, value);
};
const setLink = (ctx, node) => {
    const props = node.props || {};
    if ('src' in props && isString(props.src))
        return renderLink(ctx, node, props.src);
    if ('href' in props && isString(props.href))
        return renderLink(ctx, node, props.href);
};

const setDestination = (ctx, node) => {
    if (!node.box)
        return;
    if (!node.props)
        return;
    if ('id' in node.props) {
        ctx.addNamedDestination(node.props.id, 'XYZ', null, node.box.top, null);
    }
};

const clean = (options) => {
    const opt = { ...options };
    // We need to ensure the elements are no present if not true
    Object.entries(opt).forEach((pair) => {
        if (!pair[1]) {
            delete opt[pair[0]];
        }
    });
    return opt;
};
const parseCommonFormOptions = (node) => {
    // Common Options
    return {
        required: node.props?.required || false,
        noExport: node.props?.noExport || false,
        readOnly: node.props?.readOnly || false,
        value: node.props?.value || undefined,
        defaultValue: node.props?.defaultValue || undefined,
    };
};
const parseTextInputOptions = (node, fieldSet) => {
    return clean({
        ...parseCommonFormOptions(node),
        parent: fieldSet || undefined,
        align: node.props?.align || 'left',
        multiline: node.props?.multiline || undefined,
        password: node.props?.password || false,
        noSpell: node.props?.noSpell || false,
        format: node.props?.format || undefined,
        fontSize: node.props?.fontSize || undefined,
        MaxLen: node.props?.maxLength || undefined,
    });
};
const parseSelectAndListFieldOptions = (node) => {
    return clean({
        ...parseCommonFormOptions(node),
        sort: node.props?.sort || false,
        edit: node.props?.edit || false,
        multiSelect: node.props?.multiSelect || false,
        noSpell: node.props?.noSpell || false,
        select: node.props?.select || [''],
    });
};
const getAppearance = (ctx, codepoint, width, height) => {
    const appearance = ctx.ref({
        Type: 'XObject',
        Subtype: 'Form',
        BBox: [0, 0, width, height],
        Resources: {
            ProcSet: ['PDF', 'Text', 'ImageB', 'ImageC', 'ImageI'],
            Font: {
                ZaDi: ctx._acroform.fonts.ZaDi,
            },
        },
    });
    appearance.initDeflate();
    appearance.write(`/Tx BMC\nq\n/ZaDi ${height * 0.8} Tf\nBT\n${width * 0.45} ${height / 4} Td (${codepoint}) Tj\nET\nQ\nEMC`);
    appearance.end(null);
    return appearance;
};
const parseCheckboxOptions = (ctx, node, fieldSet) => {
    const { width, height } = node.box || {};
    const onOption = node.props?.onState || 'Yes';
    const offOption = node.props?.offState || 'Off';
    const xMark = node.props?.xMark || false;
    if (!Object.prototype.hasOwnProperty.call(ctx._acroform.fonts, 'ZaDi')) {
        const ref = ctx.ref({
            Type: 'Font',
            Subtype: 'Type1',
            BaseFont: 'ZapfDingbats',
        });
        ctx._acroform.fonts.ZaDi = ref;
        ref.end(null);
    }
    const normalAppearance = {
        [onOption]: getAppearance(ctx, xMark ? '8' : '4', width, height),
        [offOption]: getAppearance(ctx, xMark ? ' ' : '8', width, height),
    };
    return clean({
        ...parseCommonFormOptions(node),
        backgroundColor: node.props?.backgroundColor || undefined,
        borderColor: node.props?.borderColor || undefined,
        parent: fieldSet || undefined,
        value: `/${node.props?.checked === true ? onOption : offOption}`,
        defaultValue: `/${node.props?.checked === true ? onOption : offOption}`,
        AS: node.props?.checked === true ? onOption : offOption,
        AP: { N: normalAppearance, D: normalAppearance },
    });
};

const renderTextInput = (ctx, node, options) => {
    if (!node.box)
        return;
    const { top, left, width, height } = node.box;
    // Element's name
    const name = node.props?.name || '';
    const fieldSetOptions = options.fieldSets?.at(0);
    if (!ctx._root.data.AcroForm) {
        ctx.initForm();
    }
    ctx.formText(name, left, top, width, height, parseTextInputOptions(node, fieldSetOptions));
};

const renderSelect = (ctx, node) => {
    if (!node.box)
        return;
    const { top, left, width, height } = node.box;
    // Element's name
    const name = node.props?.name || '';
    if (!ctx._root.data.AcroForm) {
        ctx.initForm();
    }
    ctx.formCombo(name, left, top, width, height, parseSelectAndListFieldOptions(node));
};

const renderFieldSet = (ctx, node, options) => {
    const name = node.props?.name || '';
    if (!ctx._root.data.AcroForm) {
        ctx.initForm();
    }
    const formField = ctx.formField(name);
    const option = options;
    if (!option.fieldSets) {
        option.fieldSets = [formField];
    }
    else {
        option.fieldSets.push(formField);
    }
};
const cleanUpFieldSet = (_ctx, _node, options) => {
    options.fieldSets.pop();
};

const renderList = (ctx, node) => {
    if (!node.box)
        return;
    const { top, left, width, height } = node.box || {};
    // Element's name
    const name = ('name' in node.props ? node.props.name || '' : '');
    if (!ctx._root.data.AcroForm) {
        ctx.initForm();
    }
    ctx.formList(name, left, top, width, height, parseSelectAndListFieldOptions(node));
};

const renderCheckbox = (ctx, node, options) => {
    if (!node.box)
        return;
    const { top, left, width, height } = node.box;
    // Element's name
    const name = node.props?.name || '';
    const fieldSetOptions = options.fieldSets?.at(0);
    if (!ctx._root.data.AcroForm) {
        ctx.initForm();
    }
    ctx.formCheckbox(name, left, top, width, height, parseCheckboxOptions(ctx, node, fieldSetOptions));
};

const isRecursiveNode = (node) => node.type !== P.Text && node.type !== P.Svg;
const renderChildren = (ctx, node, options) => {
    ctx.save();
    if (node.box) {
        ctx.translate(node.box.left, node.box.top);
    }
    const children = node.children || [];
    const renderChild = (child) => renderNode(ctx, child, options);
    children.forEach(renderChild);
    ctx.restore();
};
const renderFns = {
    [P.Text]: renderText,
    [P.Note]: renderNote,
    [P.Image]: renderImage,
    [P.FieldSet]: renderFieldSet,
    [P.TextInput]: renderTextInput,
    [P.Select]: renderSelect,
    [P.Checkbox]: renderCheckbox,
    [P.List]: renderList,
    [P.Canvas]: renderCanvas,
    [P.Svg]: renderSvg,
    [P.Link]: setLink,
};
const cleanUpFns = {
    [P.FieldSet]: cleanUpFieldSet,
};
const renderNode = (ctx, node, options) => {
    const overflowHidden = node.style?.overflow === 'hidden';
    const shouldRenderChildren = isRecursiveNode(node);
    if (node.type === P.Page)
        renderPage(ctx, node);
    ctx.save();
    if (overflowHidden)
        clipNode(ctx, node);
    applyTransformations(ctx, node);
    renderBackground(ctx, node);
    renderBorders(ctx, node);
    const renderFn = renderFns[node.type];
    if (renderFn)
        renderFn(ctx, node, options);
    if (shouldRenderChildren)
        renderChildren(ctx, node, options);
    const cleanUpFn = cleanUpFns[node.type];
    if (cleanUpFn)
        cleanUpFn(ctx, node, options);
    setDestination(ctx, node);
    renderDebug(ctx, node);
    ctx.restore();
};

const addNodeBookmark = (ctx, node, pageNumber, registry) => {
    if (!node.box)
        return;
    if (!node.props)
        return;
    if ('bookmark' in node.props && node.props.bookmark) {
        const bookmark = node.props.bookmark;
        const { title, parent, expanded, zoom, fit } = bookmark;
        const outline = registry[parent] || ctx.outline;
        const top = bookmark.top || node.box.top;
        const left = bookmark.left || node.box.left;
        const instance = outline.addItem(title, {
            pageNumber,
            expanded,
            top,
            left,
            zoom,
            fit,
        });
        registry[bookmark.ref] = instance;
    }
    if (!node.children)
        return;
    node.children.forEach((child) => addNodeBookmark(ctx, child, pageNumber, registry));
};
const addBookmarks = (ctx, root) => {
    const registry = {};
    const pages = root.children || [];
    pages.forEach((page, i) => {
        addNodeBookmark(ctx, page, i, registry);
    });
};

const render = (ctx, doc) => {
    const pages = doc.children || [];
    const options = { imageCache: new Map(), fieldSets: [] };
    pages.forEach((page) => renderNode(ctx, page, options));
    addBookmarks(ctx, doc);
    ctx.end();
    return ctx;
};

export { render as default };
