#version 330 core
// Mosaic pixelate shader (docs-vn/rd/windows/plan/2026-08-02-mosaic.md §5).
//
// Samples a BackdropCapture-produced texture -- GPU-native storage,
// same convention as ClipPlate::tex / plate_passthrough.fs (row 0 at
// the BOTTOM, hence the `1.0 - v` flip below) -- after snapping the
// sample position to a grid cell centre. This is Mac's
// mosaicProcess()/snapToGrid for the Square / Triangle / Hexagon
// styles (Mosaic.md MOSAIC_TYPE_SQUARE/TRIANGLE/HEXAGON).
//
// Glass (MOSAIC_TYPE_GLASS) never reaches this shader with u_style==5:
// the caller resolves it to Square first, matching Mac's own runtime
// (its mosaic dispatch has no distinct Glass branch -- see plan §4).
// Blur (MOSAIC_TYPE_BLUR) is a separate two-pass gaussian path -- see
// mosaic_shape_composite.fs, which shares this file's mask cutout.
//
// The shape cutout comes entirely from `u_maskTex` (MosaicModel::
// maskSvgContent), the cross-platform SVG mask Mac and Android also
// write -- CAWP-17978 removed the former Windows-only analytic shape
// enum, whose branch the mask texture always won over anyway. Outside
// the mask, alpha=0 so the premultiplied
// SRC_OVER blend the caller uses is a no-op there -- the canvas content
// already drawn beneath this Mosaic layer shows through unchanged,
// exactly like Mac's alphaComposite(base: processedBackground,
// alphaMask: shapeMask) (plan §5.1).
//
// Paired vertex shader: transition_dissolve.vs (plain passthrough --
// no dedicated .vs needed for a plain full-quad sampler).

out vec4 FragColor;
in  vec2 TexCoord;   // [0,1] over the dst-rect-sized viewport

uniform sampler2D u_backdrop;
// Fraction of the (possibly larger, never-shrink pooled) capture
// texture that actually holds this capture's valid pixels --
// vec2(dstW / poolW, dstH / poolH). 1,1 when the pool is exactly
// dstW x dstH. Same never-shrink-scratch convention as blur.vs /
// mask.vs's own u_texScale.
uniform vec2  u_texScale;
// Dst rect size in pixels (dstW, dstH) -- for correct cell aspect and
// to convert the pixel-space grid snap back to a [0,1] UV.
uniform vec2  u_dstSizePx;
// Grid cell size, in dst-rect pixels (Mac §4.2's kernel size).
uniform float u_cellPx;
// 1=Square 2=Triangle 3=Hexagon.
uniform int   u_style;
// Layer opacity, [0,1] -- multiplied into the final alpha alongside the
// shape cutout (see Compositor.cpp's PhotoLayerRgba::opacity).
uniform float u_opacity;
// Custom cross-platform SVG mask (MosaicModel::maskSvgContent), rasterised
// by GpuRasterPrimitive::resolveMosaicMaskTexture -- alpha channel only,
// sampled directly at TexCoord with no flip: the source QImage is
// uploaded row-0-first with no mirroring, so texture V=0 already equals
// the SVG's top row, matching TexCoord.y==0 == this dst rect's top.
// u_useMaskTex is 0 only when the slice carries no mask at all (empty
// svgContent, or an SVG that failed to parse), in which case the whole
// dst rect is filled uncropped.
uniform sampler2D u_maskTex;
uniform int   u_useMaskTex;

vec2 snapSquare(vec2 px, float cell) {
    return (floor(px / cell) + 0.5) * cell;
}

// Equilateral triangle tiling (CAWP-17713): alternating up/down
// triangles (△▽), edge length `cell`, matching the standard "triangle
// wave" mosaic tessellation -- NOT a right-triangle diagonal split.
// Numerically verified in Python before porting here (all 3 sides of
// both candidate triangles measure exactly `cell`; a dense scan finds
// full coverage with no gaps/overlaps and clean alternation).
//
// Row height h = cell*sqrt(3)/2 (an equilateral triangle's own
// height). Within a row: apex-up triangles have their base on the
// row's BOTTOM edge (width `cell`, apex at the top-mid point);
// apex-down triangles have their base on the TOP edge (apex at the
// bottom-mid point). Side by side at half-`cell` intervals they
// interlock with no gaps. `local.x` is left unwrapped (not modulo'd
// into [0,halfCell)) so a single point-in-triangle test against the
// apex-up candidate based at the nearest even half-cell boundary
// unambiguously decides up-vs-down -- no separate column-parity
// branch needed, unlike a naive per-cell diagonal split.
vec2 snapTriangle(vec2 px, float cell) {
    const float kSqrt3 = 1.7320508075688772;
    float h = cell * kSqrt3 * 0.5;
    float halfCell = cell * 0.5;

    float row = floor(px.y / h);
    vec2  rowOrigin = vec2(0.0, row * h);
    vec2  local = px - rowOrigin;   // local.y in [0,h); local.x unrestricted

    // Apex-up candidate based at the nearest even half-cell boundary
    // at-or-left of local.x.
    float colUpBase = 2.0 * floor(floor(local.x / halfCell) * 0.5);
    float baseUpX   = colUpBase * halfCell;
    vec2  upApex  = vec2(baseUpX + halfCell, 0.0);
    vec2  upBaseL = vec2(baseUpX, h);
    vec2  upBaseR = vec2(baseUpX + cell, h);

    // Point-in-triangle via edge functions (cross-product z-component
    // of each edge against the point); all 3 same sign => inside.
    float eUp1 = (upBaseL.x - upApex.x)  * (local.y - upApex.y)  - (upBaseL.y - upApex.y)  * (local.x - upApex.x);
    float eUp2 = (upBaseR.x - upBaseL.x) * (local.y - upBaseL.y) - (upBaseR.y - upBaseL.y) * (local.x - upBaseL.x);
    float eUp3 = (upApex.x  - upBaseR.x) * (local.y - upBaseR.y) - (upApex.y  - upBaseR.y) * (local.x - upBaseR.x);
    bool insideUp = (eUp1 >= 0.0 && eUp2 >= 0.0 && eUp3 >= 0.0)
                 || (eUp1 <= 0.0 && eUp2 <= 0.0 && eUp3 <= 0.0);

    if (insideUp) {
        return rowOrigin + (upApex + upBaseL + upBaseR) / 3.0;
    }

    // Otherwise the point is in the apex-down triangle immediately to
    // one side of the apex-up candidate -- one half-cell further
    // right if local.x overshot the apex-up triangle's own right
    // edge, one half-cell to the left otherwise.
    float colDownBase = (local.x >= baseUpX + halfCell) ? colUpBase + 1.0 : colUpBase - 1.0;
    float baseDownX = colDownBase * halfCell;
    vec2  downApex  = vec2(baseDownX + halfCell, h);
    vec2  downBaseL = vec2(baseDownX, 0.0);
    vec2  downBaseR = vec2(baseDownX + cell, 0.0);
    return rowOrigin + (downApex + downBaseL + downBaseR) / 3.0;
}

// Exact hex-Voronoi tiling: snaps to the centre of the flat-top hexagon
// (circumradius `size`) that contains `px`, using the standard cube/axial
// "round to nearest hex" algorithm (fractional axial coords -> nearest cube
// -> largest-rounding-error component reconstructed from the other two, so
// the three coords still sum to zero). `size` is chosen so the hexagon's
// WIDTH (2*size) matches `cell`, keeping the same visual density as the
// square/triangle grids for a given u_cellPx.
vec2 snapHexagon(vec2 px, float cell) {
    float size = cell * 0.5;

    // Flat-top axial -> pixel: x = size*1.5*q, y = size*sqrt(3)*(r + q/2).
    float q = (px.x / size) * (2.0 / 3.0);
    float r = (px.y / (size * 1.7320508)) - q * 0.5;

    // Axial (q,r) -> cube (x,y,z), x+y+z=0.
    float cx = q;
    float cz = r;
    float cy = -cx - cz;

    float rx = round(cx);
    float ry = round(cy);
    float rz = round(cz);

    float dx = abs(rx - cx);
    float dy = abs(ry - cy);
    float dz = abs(rz - cz);

    if (dx > dy && dx > dz)      rx = -ry - rz;
    else if (dy > dz)            ry = -rx - rz;
    else                         rz = -rx - ry;

    // Cube -> axial -> pixel (rx,rz are the rounded axial q,r).
    float centerX = size * 1.5 * rx;
    float centerY = size * 1.7320508 * (rz + rx * 0.5);
    return vec2(centerX, centerY);
}

void main() {
    vec2 px = TexCoord * u_dstSizePx;
    vec2 snapped;
    if (u_style == 2)      snapped = snapTriangle(px, u_cellPx);
    else if (u_style == 3) snapped = snapHexagon(px, u_cellPx);
    else                   snapped = snapSquare(px, u_cellPx);

    vec2 uv01 = clamp(snapped / u_dstSizePx, 0.0, 1.0);
    vec2 uv   = vec2(uv01.x, 1.0 - uv01.y) * u_texScale;
    vec4 color = texture(u_backdrop, uv);

    float shapeCutout = (u_useMaskTex == 1)
        ? texture(u_maskTex, TexCoord).a
        : 1.0;
    float alpha = shapeCutout * u_opacity;
    FragColor = vec4(color.rgb * alpha, color.a * alpha);
}
