"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CanvasRenderer = void 0;
const stacking_context_1 = require("../stacking-context");
const color_utilities_1 = require("../../css/types/color-utilities");
const path_1 = require("../path");
const bound_curves_1 = require("../bound-curves");
const background_1 = require("../background");
const box_sizing_1 = require("../box-sizing");
const font_metrics_1 = require("../font-metrics");
const text_renderer_1 = require("./text-renderer");
const background_renderer_1 = require("./background-renderer");
const border_renderer_1 = require("./border-renderer");
const border_image_renderer_1 = require("./border-image-renderer");
const effects_renderer_1 = require("./effects-renderer");
const canvas_path_1 = require("./canvas-path");
const object_fit_1 = require("../object-fit");
const content_renderer_1 = require("./content-renderer");
const constants_1 = require("../../core/constants");
class CanvasRenderer {
    constructor(context, options) {
        this.context = context;
        this.options = options;
        this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
        const ctx = this.canvas.getContext('2d');
        if (!ctx) {
            throw new Error('Failed to get 2D rendering context from canvas');
        }
        this.ctx = ctx;
        if (!options.canvas) {
            this.canvas.width = Math.floor(options.width * options.scale);
            this.canvas.height = Math.floor(options.height * options.scale);
            this.canvas.style.width = `${options.width}px`;
            this.canvas.style.height = `${options.height}px`;
        }
        this.fontMetrics = new font_metrics_1.FontMetrics(document);
        this.ctx.scale(this.options.scale, this.options.scale);
        this.ctx.translate(-options.x, -options.y);
        this.ctx.textBaseline = 'bottom';
        // Set image smoothing options
        if (options.imageSmoothing !== undefined) {
            this.ctx.imageSmoothingEnabled = options.imageSmoothing;
        }
        if (options.imageSmoothingQuality) {
            this.ctx.imageSmoothingQuality = options.imageSmoothingQuality;
        }
        // Initialize specialized renderers
        this.backgroundRenderer = new background_renderer_1.BackgroundRenderer({
            ctx: this.ctx,
            context: this.context,
            canvas: this.canvas,
            options: {
                width: options.width,
                height: options.height,
                scale: options.scale
            }
        });
        this.borderRenderer = new border_renderer_1.BorderRenderer({ ctx: this.ctx }, {
            path: (paths) => this.path(paths),
            formatPath: (paths) => this.formatPath(paths)
        });
        this.borderImageRenderer = new border_image_renderer_1.BorderImageRenderer(this.ctx);
        this.effectsRenderer = new effects_renderer_1.EffectsRenderer({ ctx: this.ctx }, { path: (paths) => this.path(paths) });
        this.textRenderer = new text_renderer_1.TextRenderer({
            ctx: this.ctx,
            context: this.context,
            options: { scale: options.scale }
        });
        this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
    }
    async renderStack(stack) {
        const styles = stack.element.container.styles;
        if (styles.isVisible()) {
            await this.renderStackContent(stack);
        }
    }
    async renderNode(paint) {
        if (paint.container.debugRender) {
            debugger;
        }
        if (paint.container.styles.isVisible()) {
            await this.renderNodeBackgroundAndBorders(paint);
            await this.renderNodeContent(paint);
        }
    }
    /**
     * Helper method to render text with paint order support
     * Reduces code duplication in line-clamp and normal rendering
     */
    // Helper method to truncate text and add ellipsis if needed
    renderReplacedElement(container, curves, image) {
        const intrinsicWidth = image.naturalWidth || container.intrinsicWidth;
        const intrinsicHeight = image.naturalHeight || container.intrinsicHeight;
        if (image && intrinsicWidth > 0 && intrinsicHeight > 0) {
            const box = (0, box_sizing_1.contentBox)(container);
            const path = (0, bound_curves_1.calculatePaddingBoxPath)(curves);
            this.path(path);
            this.ctx.save();
            this.ctx.clip();
            const { sx, sy, sw, sh, dx, dy, dw, dh } = (0, object_fit_1.calculateObjectFitRendering)(intrinsicWidth, intrinsicHeight, box, container.styles.objectFit, container.styles.objectPosition);
            this.ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
            this.ctx.restore();
        }
    }
    async renderNodeContent(paint) {
        this.effectsRenderer.applyEffects(paint.getEffects(4 /* EffectTarget.CONTENT */));
        const container = paint.container;
        const curves = paint.curves;
        const styles = container.styles;
        // Use content box for text overflow calculation (excludes padding and border)
        const textBounds = (0, box_sizing_1.contentBox)(container);
        for (const child of container.textNodes) {
            await this.textRenderer.renderTextNode(child, styles, textBounds);
        }
        await (0, content_renderer_1.renderReplacedElements)(this.ctx, this.context, {
            scale: this.options.scale,
            backgroundColor: this.options.backgroundColor,
            x: this.options.x,
            y: this.options.y,
            width: this.options.width,
            height: this.options.height
        }, (ctx, opts) => new CanvasRenderer(ctx, opts), container, curves, styles, (c, cv, img) => this.renderReplacedElement(c, cv, img));
        (0, content_renderer_1.renderFormElements)(this.ctx, this.fontMetrics, this.textRenderer, this.path.bind(this), container, styles);
        await (0, content_renderer_1.renderListMarker)(this.ctx, this.context, this.textRenderer, paint, container, styles);
    }
    async renderStackContent(stack) {
        if (stack.element.container.debugRender) {
            debugger;
        }
        const signal = this.options.signal;
        // https://www.w3.org/TR/css-position-3/#painting-order
        // 1. the background and borders of the element forming the stacking context.
        await this.renderNodeBackgroundAndBorders(stack.element);
        // 2. the child stacking contexts with negative stack levels (most negative first).
        for (const child of stack.negativeZIndex) {
            if (signal?.aborted)
                throw new DOMException('The operation was aborted.', 'AbortError');
            await this.renderStack(child);
        }
        // 3. For all its in-flow, non-positioned, block-level descendants in tree order:
        if (signal?.aborted)
            throw new DOMException('The operation was aborted.', 'AbortError');
        await this.renderNodeContent(stack.element);
        for (const child of stack.nonInlineLevel) {
            await this.renderNode(child);
        }
        // 4. All non-positioned floating descendants, in tree order. For each one of these,
        // treat the element as if it created a new stacking context, but any positioned descendants and descendants
        // which actually create a new stacking context should be considered part of the parent stacking context,
        // not this new one.
        for (const child of stack.nonPositionedFloats) {
            if (signal?.aborted)
                throw new DOMException('The operation was aborted.', 'AbortError');
            await this.renderStack(child);
        }
        // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
        for (const child of stack.nonPositionedInlineLevel) {
            if (signal?.aborted)
                throw new DOMException('The operation was aborted.', 'AbortError');
            await this.renderStack(child);
        }
        for (const child of stack.inlineLevel) {
            await this.renderNode(child);
        }
        // 6. All positioned, opacity or transform descendants, in tree order that fall into the following categories:
        //  All positioned descendants with 'z-index: auto' or 'z-index: 0', in tree order.
        //  For those with 'z-index: auto', treat the element as if it created a new stacking context,
        //  but any positioned descendants and descendants which actually create a new stacking context should be
        //  considered part of the parent stacking context, not this new one. For those with 'z-index: 0',
        //  treat the stacking context generated atomically.
        //
        //  All opacity descendants with opacity less than 1
        //
        //  All transform descendants with transform other than none
        for (const child of stack.zeroOrAutoZIndexOrTransformedOrOpacity) {
            if (signal?.aborted)
                throw new DOMException('The operation was aborted.', 'AbortError');
            await this.renderStack(child);
        }
        // 7. Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index
        // order (smallest first) then tree order.
        for (const child of stack.positiveZIndex) {
            if (signal?.aborted)
                throw new DOMException('The operation was aborted.', 'AbortError');
            await this.renderStack(child);
        }
    }
    mask(paths) {
        this.ctx.beginPath();
        this.ctx.moveTo(0, 0);
        // Use logical dimensions (options.width/height) instead of canvas pixel dimensions
        // because context has already been scaled by this.options.scale
        // Fix for Issue #126: Using canvas pixel dimensions causes broken output
        this.ctx.lineTo(this.options.width, 0);
        this.ctx.lineTo(this.options.width, this.options.height);
        this.ctx.lineTo(0, this.options.height);
        this.ctx.lineTo(0, 0);
        this.formatPath(paths.slice(0).reverse());
        this.ctx.closePath();
    }
    path(paths) {
        (0, canvas_path_1.createCanvasPath)(this.ctx, paths);
    }
    formatPath(paths) {
        (0, canvas_path_1.formatCanvasPath)(this.ctx, paths);
    }
    async renderNodeBackgroundAndBorders(paint) {
        this.effectsRenderer.applyEffects(paint.getEffects(2 /* EffectTarget.BACKGROUND_BORDERS */));
        const styles = paint.container.styles;
        const hasBackground = !(0, color_utilities_1.isTransparent)(styles.backgroundColor) || styles.backgroundImage.length;
        const borders = [
            { style: styles.borderTopStyle, color: styles.borderTopColor, width: styles.borderTopWidth },
            { style: styles.borderRightStyle, color: styles.borderRightColor, width: styles.borderRightWidth },
            { style: styles.borderBottomStyle, color: styles.borderBottomColor, width: styles.borderBottomWidth },
            { style: styles.borderLeftStyle, color: styles.borderLeftColor, width: styles.borderLeftWidth }
        ];
        const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea((0, background_1.getBackgroundValueForIndex)(styles.backgroundClip, 0), paint.curves);
        if (hasBackground || styles.boxShadow.length) {
            this.ctx.save();
            this.path(backgroundPaintingArea);
            this.ctx.clip();
            if (!(0, color_utilities_1.isTransparent)(styles.backgroundColor)) {
                this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.backgroundColor);
                this.ctx.fill();
            }
            await this.backgroundRenderer.renderBackgroundImage(paint.container);
            this.ctx.restore();
            styles.boxShadow
                .slice(0)
                .reverse()
                .forEach((shadow) => {
                this.ctx.save();
                const borderBoxArea = (0, bound_curves_1.calculateBorderBoxPath)(paint.curves);
                const maskOffset = shadow.inset ? 0 : constants_1.SHADOW_MASK_OFFSET;
                const shadowPaintingArea = (0, path_1.transformPath)(borderBoxArea, -maskOffset + (shadow.inset ? 1 : -1) * shadow.spread.number, (shadow.inset ? 1 : -1) * shadow.spread.number, shadow.spread.number * (shadow.inset ? -2 : 2), shadow.spread.number * (shadow.inset ? -2 : 2));
                if (shadow.inset) {
                    this.path(borderBoxArea);
                    this.ctx.clip();
                    this.mask(shadowPaintingArea);
                }
                else {
                    this.mask(borderBoxArea);
                    this.ctx.clip();
                    this.path(shadowPaintingArea);
                }
                this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
                this.ctx.shadowOffsetY = shadow.offsetY.number;
                this.ctx.shadowColor = (0, color_utilities_1.asString)(shadow.color);
                this.ctx.shadowBlur = shadow.blur.number;
                this.ctx.fillStyle = shadow.inset ? (0, color_utilities_1.asString)(shadow.color) : 'rgba(0,0,0,1)';
                this.ctx.fill();
                this.ctx.restore();
            });
        }
        // Render border-image if present (replaces traditional borders per CSS spec)
        if (styles.borderImageSource) {
            const source = styles.borderImageSource;
            if (source.type === 0 /* CSSImageType.URL */) {
                const url = source.url;
                try {
                    const image = await this.context.cache.match(url);
                    if (image) {
                        const bounds = paint.container.bounds;
                        this.borderImageRenderer.renderBorderImage(bounds, image, styles.borderImageSlice, styles.borderImageRepeat, Math.max(0, styles.borderTopWidth), Math.max(0, styles.borderRightWidth), Math.max(0, styles.borderBottomWidth), Math.max(0, styles.borderLeftWidth));
                    }
                }
                catch (e) {
                    this.context.logger.error(`Error loading border-image ${url}`);
                }
            }
            // When border-image is present, skip regular border rendering
            return;
        }
        let side = 0;
        for (const border of borders) {
            if (border.style !== 0 /* BORDER_STYLE.NONE */ && !(0, color_utilities_1.isTransparent)(border.color) && border.width > 0) {
                if (border.style === 2 /* BORDER_STYLE.DASHED */) {
                    await this.borderRenderer.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 2 /* BORDER_STYLE.DASHED */);
                }
                else if (border.style === 3 /* BORDER_STYLE.DOTTED */) {
                    await this.borderRenderer.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 3 /* BORDER_STYLE.DOTTED */);
                }
                else if (border.style === 4 /* BORDER_STYLE.DOUBLE */) {
                    await this.borderRenderer.renderDoubleBorder(border.color, border.width, side, paint.curves);
                }
                else {
                    await this.borderRenderer.renderSolidBorder(border.color, side, paint.curves);
                }
            }
            side++;
        }
    }
    async render(element) {
        if (this.options.backgroundColor) {
            this.ctx.fillStyle = (0, color_utilities_1.asString)(this.options.backgroundColor);
            this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
        }
        const stack = (0, stacking_context_1.parseStackingContexts)(element);
        await this.renderStack(stack);
        this.effectsRenderer.applyEffects([]);
        return this.canvas;
    }
}
exports.CanvasRenderer = CanvasRenderer;
const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
    switch (clip) {
        case 0 /* BACKGROUND_CLIP.BORDER_BOX */:
            return (0, bound_curves_1.calculateBorderBoxPath)(curves);
        case 2 /* BACKGROUND_CLIP.CONTENT_BOX */:
            return (0, bound_curves_1.calculateContentBoxPath)(curves);
        case 1 /* BACKGROUND_CLIP.PADDING_BOX */:
        default:
            return (0, bound_curves_1.calculatePaddingBoxPath)(curves);
    }
};
