/**
 * Strip-based trapezoid perspective for raster layers. Canvas 2D is affine-only,
 * so we fake axial rotation by slicing the source along the perpendicular axis
 * and drawing each strip with its own cross-axis scale — the far edge shrinks,
 * producing a proper trapezoid (not a parallelogram). Good enough for App Store
 * mockup tilts up to ~45°.
 *
 * Only one axis is rendered at a time; when both tiltX and tiltY are set the
 * dominant (larger |angle|) wins. Compound bilinear perspective is v2.
 *
 * Filters are not applied here; the caller either passes a pre-filtered canvas
 * as `img` or accepts that tilt skips the Konva filter pipeline.
 */

const STRIP_COUNT_MIN = 60;
const STRIP_COUNT_MAX = 2000;

/**
 * Real-frame PNG tilt cap. Beyond ±15° per axis the strip sampler starts
 * showing visible staircase artifacts along the chassis silhouette: the
 * photoreal PNG has hard edges (rendered camera bumps, speaker grilles)
 * that the per-strip cross-axis scale snaps into discrete jumps. Inside
 * the cap the strip count + sub-pixel bleed (see `stripCount` + bleed
 * math in drawTiltX/Y) keep the staircase below ~1px at common export
 * resolutions, so the tilt reads as a clean continuous plane. The
 * procedural bezel has no such cap because its edges are programmatic
 * (already anti-aliased by the path-fill pass) — only the PNG branch
 * cares about the cap. Symmetric across both axes; the dominant-axis
 * picker in `drawTilt` already handles compound tilt by rendering the
 * larger angle's axis.
 */
export const REAL_FRAME_TILT_LIMIT = 15;

export function realFrameTiltAllowed(tiltX: number, tiltY: number): boolean {
    return (
        Math.abs(tiltX) <= REAL_FRAME_TILT_LIMIT
        && Math.abs(tiltY) <= REAL_FRAME_TILT_LIMIT
    );
}

// Each strip is drawn with a single cross-axis height (width for tiltX), while
// the true trapezoid edge varies continuously. The staircase step at each
// strip boundary is Δ ≈ shrink · dstCrossAxis / N. To keep the step sub-pixel
// at any reasonable zoom, scale N with the actual shrink amount in dest pixels.
function stripCount(shrinkPx: number): number {
    return Math.min(STRIP_COUNT_MAX, Math.max(STRIP_COUNT_MIN, Math.ceil(shrinkPx * 1.5)));
}

export type PerspectiveCrop = {
    x: number;
    y: number;
    width: number;
    height: number;
};

export function drawTilt(
    ctx: CanvasRenderingContext2D,
    img: CanvasImageSource,
    dstWidth: number,
    dstHeight: number,
    tiltX: number,
    tiltY: number,
    crop?: PerspectiveCrop,
): void {
    if (dstWidth <= 0 || dstHeight <= 0) return;
    const absX = Math.abs(tiltX);
    const absY = Math.abs(tiltY);
    if (absX < 0.01 && absY < 0.01) {
        const sx = crop?.x ?? 0;
        const sy = crop?.y ?? 0;
        const sw = crop?.width ?? imgWidth(img);
        const sh = crop?.height ?? imgHeight(img);
        ctx.drawImage(img, sx, sy, sw, sh, 0, 0, dstWidth, dstHeight);
        return;
    }
    if (absX > absY) {
        drawTiltX(ctx, img, dstWidth, dstHeight, tiltX, crop);
    } else {
        drawTiltY(ctx, img, dstWidth, dstHeight, tiltY, crop);
    }
}

function drawTiltY(
    ctx: CanvasRenderingContext2D,
    img: CanvasImageSource,
    dstWidth: number,
    dstHeight: number,
    angle: number,
    crop?: PerspectiveCrop,
): void {
    const theta = (angle * Math.PI) / 180;
    const sin = Math.sin(theta);
    const cos = Math.cos(theta);

    // Far-edge cross-axis shrink and primary-axis compression give the
    // trapezoid its receding shape. The 0.5/0.3 floors keep extreme angles
    // from degenerating into a line.
    const shrink = Math.abs(sin) * 0.5;
    const nearH = dstHeight;
    const farH = dstHeight * (1 - shrink);
    const leftH = angle < 0 ? nearH : farH;
    const rightH = angle < 0 ? farH : nearH;
    const effW = dstWidth * Math.max(0.3, Math.abs(cos));
    const xOffset = (dstWidth - effW) / 2;

    const sx0 = crop?.x ?? 0;
    const sy0 = crop?.y ?? 0;
    const sw = crop?.width ?? imgWidth(img);
    const sh = crop?.height ?? imgHeight(img);

    ctx.imageSmoothingEnabled = true;
    ctx.imageSmoothingQuality = 'high';

    const N = stripCount(shrink * dstHeight);
    for (let i = 0; i < N; i++) {
        const t0 = i / N;
        const t1 = (i + 1) / N;
        const tm = (t0 + t1) / 2;
        const ssx = sx0 + t0 * sw;
        const ssw = sw / N;
        const dx = xOffset + t0 * effW;
        const dw = effW / N;
        const h = leftH + (rightH - leftH) * tm;
        const dy = (dstHeight - h) / 2;
        const last = i === N - 1;
        const bleedDst = last ? 0 : 1;
        const bleedSrc = last || dw <= 0 ? 0 : ssw / dw;
        ctx.drawImage(img, ssx, sy0, ssw + bleedSrc, sh, dx, dy, dw + bleedDst, h);
    }
}

function drawTiltX(
    ctx: CanvasRenderingContext2D,
    img: CanvasImageSource,
    dstWidth: number,
    dstHeight: number,
    angle: number,
    crop?: PerspectiveCrop,
): void {
    const theta = (angle * Math.PI) / 180;
    const sin = Math.sin(theta);
    const cos = Math.cos(theta);

    // Positive angle = top recedes (looking down on the image). Mirror of
    // drawTiltY with axes swapped: slice along Y, vary strip width per row.
    const shrink = Math.abs(sin) * 0.5;
    const nearW = dstWidth;
    const farW = dstWidth * (1 - shrink);
    const topW = angle < 0 ? nearW : farW;
    const bottomW = angle < 0 ? farW : nearW;
    const effH = dstHeight * Math.max(0.3, Math.abs(cos));
    const yOffset = (dstHeight - effH) / 2;

    const sx0 = crop?.x ?? 0;
    const sy0 = crop?.y ?? 0;
    const sw = crop?.width ?? imgWidth(img);
    const sh = crop?.height ?? imgHeight(img);

    ctx.imageSmoothingEnabled = true;
    ctx.imageSmoothingQuality = 'high';

    const N = stripCount(shrink * dstWidth);
    for (let i = 0; i < N; i++) {
        const t0 = i / N;
        const t1 = (i + 1) / N;
        const tm = (t0 + t1) / 2;
        const ssy = sy0 + t0 * sh;
        const ssh = sh / N;
        const dy = yOffset + t0 * effH;
        const dh = effH / N;
        const w = topW + (bottomW - topW) * tm;
        const dx = (dstWidth - w) / 2;
        const last = i === N - 1;
        const bleedDst = last ? 0 : 1;
        const bleedSrc = last || dh <= 0 ? 0 : ssh / dh;
        ctx.drawImage(img, sx0, ssy, sw, ssh + bleedSrc, dx, dy, w, dh + bleedDst);
    }
}

function imgWidth(img: CanvasImageSource): number {
    if ('naturalWidth' in img && img.naturalWidth) return img.naturalWidth;
    if ('width' in img && typeof img.width === 'number') return img.width;
    return 0;
}

function imgHeight(img: CanvasImageSource): number {
    if ('naturalHeight' in img && img.naturalHeight) return img.naturalHeight;
    if ('height' in img && typeof img.height === 'number') return img.height;
    return 0;
}

/**
 * Trace the trapezoid outline that drawTilt renders into, so stroke, shadow,
 * and clip all follow the tilted silhouette (optionally with rounded corners)
 * instead of the underlying rect. Geometry matches the dominant axis' strip
 * envelope exactly (same shrink/effW formulas).
 */
export function buildTrapezoidPath(
    ctx: CanvasRenderingContext2D,
    x: number,
    y: number,
    width: number,
    height: number,
    tiltX: number,
    tiltY: number,
    radius: number = 0,
): void {
    if (width <= 0 || height <= 0) {
        ctx.beginPath();
        ctx.closePath();
        return;
    }
    const absX = Math.abs(tiltX);
    const absY = Math.abs(tiltY);
    if (absX < 0.01 && absY < 0.01) {
        const r = Math.max(0, Math.min(radius, width / 2, height / 2));
        ctx.beginPath();
        if (r <= 0) {
            ctx.rect(x, y, width, height);
        } else {
            ctx.moveTo(x + r, y);
            ctx.arcTo(x + width, y, x + width, y + r, r);
            ctx.arcTo(x + width, y + height, x + width - r, y + height, r);
            ctx.arcTo(x, y + height, x, y + height - r, r);
            ctx.arcTo(x, y, x + r, y, r);
        }
        ctx.closePath();
        return;
    }
    if (absX > absY) {
        buildTrapezoidPathX(ctx, x, y, width, height, tiltX, radius);
    } else {
        buildTrapezoidPathY(ctx, x, y, width, height, tiltY, radius);
    }
}

function buildTrapezoidPathY(
    ctx: CanvasRenderingContext2D,
    x: number,
    y: number,
    width: number,
    height: number,
    angle: number,
    radius: number,
): void {
    const theta = (angle * Math.PI) / 180;
    const sin = Math.sin(theta);
    const cos = Math.cos(theta);
    const shrink = Math.abs(sin) * 0.5;
    const nearH = height;
    const farH = height * (1 - shrink);
    const leftH = angle < 0 ? nearH : farH;
    const rightH = angle < 0 ? farH : nearH;
    const effW = width * Math.max(0.3, Math.abs(cos));
    const xOffset = (width - effW) / 2;
    const cy = height / 2;

    const xL = x + xOffset;
    const xR = x + xOffset + effW;
    const yLt = y + cy - leftH / 2;
    const yLb = y + cy + leftH / 2;
    const yRt = y + cy - rightH / 2;
    const yRb = y + cy + rightH / 2;

    const topLen = Math.hypot(xR - xL, yRt - yLt);
    // arcTo clamps its own radius but oversized values can snap to straight
    // lines on short edges — cap to half the shortest side for a predictable
    // result.
    const r = Math.max(0, Math.min(radius, topLen / 2, leftH / 2, rightH / 2));

    ctx.beginPath();
    if (r <= 0) {
        ctx.moveTo(xL, yLt);
        ctx.lineTo(xR, yRt);
        ctx.lineTo(xR, yRb);
        ctx.lineTo(xL, yLb);
    } else {
        const midTopX = (xL + xR) / 2;
        const midTopY = (yLt + yRt) / 2;
        ctx.moveTo(midTopX, midTopY);
        ctx.arcTo(xR, yRt, xR, yRb, r);
        ctx.arcTo(xR, yRb, xL, yLb, r);
        ctx.arcTo(xL, yLb, xL, yLt, r);
        ctx.arcTo(xL, yLt, xR, yRt, r);
    }
    ctx.closePath();
}

function buildTrapezoidPathX(
    ctx: CanvasRenderingContext2D,
    x: number,
    y: number,
    width: number,
    height: number,
    angle: number,
    radius: number,
): void {
    const theta = (angle * Math.PI) / 180;
    const sin = Math.sin(theta);
    const cos = Math.cos(theta);
    const shrink = Math.abs(sin) * 0.5;
    const nearW = width;
    const farW = width * (1 - shrink);
    const topW = angle < 0 ? nearW : farW;
    const bottomW = angle < 0 ? farW : nearW;
    const effH = height * Math.max(0.3, Math.abs(cos));
    const yOffset = (height - effH) / 2;
    const cx = width / 2;

    const yT = y + yOffset;
    const yB = y + yOffset + effH;
    const xTl = x + cx - topW / 2;
    const xTr = x + cx + topW / 2;
    const xBl = x + cx - bottomW / 2;
    const xBr = x + cx + bottomW / 2;

    const leftLen = Math.hypot(xBl - xTl, yB - yT);
    const r = Math.max(0, Math.min(radius, leftLen / 2, topW / 2, bottomW / 2));

    ctx.beginPath();
    if (r <= 0) {
        ctx.moveTo(xTl, yT);
        ctx.lineTo(xTr, yT);
        ctx.lineTo(xBr, yB);
        ctx.lineTo(xBl, yB);
    } else {
        const midTopX = (xTl + xTr) / 2;
        ctx.moveTo(midTopX, yT);
        ctx.arcTo(xTr, yT, xBr, yB, r);
        ctx.arcTo(xBr, yB, xBl, yB, r);
        ctx.arcTo(xBl, yB, xTl, yT, r);
        ctx.arcTo(xTl, yT, xTr, yT, r);
    }
    ctx.closePath();
}
