// Shaders/Stitch.metal.txt (Metal Shading Language; .txt so Xcode ships it as a resource instead of requiring
// the Metal Toolchain at build time) — tasks 4.1/4.2.
// Thread look: each stitch is a lens-shaped capsule (full width mid-stitch, pinched at the needle points),
// shaded as a cylinder with a Kajiya-Kay anisotropic highlight along the thread direction and a subtle twist.
#include <metal_stdlib>
using namespace metal;

struct Segment {            // matches SatinsmithRender.GPUSegment
    float2 a;
    float2 b;
    float4 color;
};

struct Uniforms {           // matches SatinsmithRender.GPUUniforms
    float2 scale;           // mm → clip
    float2 offset;          // clip translation
    float  halfWidthMM;
    float  pxPerMM;         // for 1-px anti-aliasing in mm units
    uint   lodStride;       // draw every n-th stitch when the design is a sub-pixel blob (overdraw cap)
    float  _pad1;
    float3 light;           // normalized, towards the light
    float  _pad2;
    float4x4 mvp;           // 3D tilt (4.6) applied in clip space; identity when flat
};

struct VOut {
    float4 position [[position]];
    float4 color;
    float2 local;           // (along mm from a, across mm) in the stitch frame
    float  len;             // stitch length mm
    float  hw;              // half width mm
    float2 dir;             // unit direction (design space, Y down)
};

vertex VOut stitch_vertex(uint vid [[vertex_id]], uint iid [[instance_id]],
                          const device Segment *segs [[buffer(0)]],
                          constant Uniforms &u [[buffer(1)]]) {
    VOut o;
    if (u.lodStride > 1 && (iid % u.lodStride) != 0) {          // culled by LOD: clip everything
        o.position = float4(0, 0, -2, 1); o.color = 0; o.local = 0; o.len = 0; o.hw = 0; o.dir = float2(1, 0);
        return o;
    }
    Segment s = segs[iid];
    float2 d = s.b - s.a;
    float len = length(d);
    float2 dir = len > 1e-5 ? d / len : float2(1, 0);
    float2 n = float2(-dir.y, dir.x);
    float hw = u.halfWidthMM;
    // Quad covers the capsule plus a 1 px AA margin.
    float m = hw + 1.0 / u.pxPerMM;
    float2 corners[6] = { s.a - dir * m - n * m, s.a - dir * m + n * m, s.b + dir * m - n * m,
                          s.b + dir * m - n * m, s.a - dir * m + n * m, s.b + dir * m + n * m };
    float2 locals[6]  = { {-m, -m}, {-m, m}, {len + m, -m},  {len + m, -m}, {-m, m}, {len + m, m} };
    o.position = u.mvp * float4(corners[vid] * u.scale + u.offset, 0, 1);
    o.color = s.color;
    o.local = locals[vid];
    o.len = len; o.hw = hw; o.dir = dir;
    return o;
}

// Width profile along the stitch: uniform, pinching to ~30 % only within ~0.5 mm of each needle penetration
// (the thread dives through the fabric there). A whole-length lens would open gaps in dense satin.
static inline float profile(float t, float len, float hw) {
    if (len < 1e-4) return hw;
    float zone = min(0.5, len * 0.25);
    float d = min(t, len - t);
    float x = clamp(d / zone, 0.0, 1.0);
    return hw * (0.30 + 0.70 * sqrt(x));
}

fragment float4 stitch_fragment(VOut in [[stage_in]], constant Uniforms &u [[buffer(1)]]) {
    float along = in.local.x, across = in.local.y;
    float px = 1.0 / u.pxPerMM;
    // Signed distance to the lens capsule: rounded ends at the needle points, lens width in between.
    float dEnd = 0.0;
    if (along < 0.0)          dEnd = -along;
    else if (along > in.len)  dEnd = along - in.len;
    float w = profile(along, in.len, in.hw);
    float capR = max(in.hw * 0.30, 0.5 * px);                        // needle-point radius
    float dist = (along < 0.0 || along > in.len) ? length(float2(dEnd, across)) - capR
                                                  : abs(across) - w;
    float alpha = 1.0 - smoothstep(-0.5 * px, 0.5 * px, dist);
    if (alpha <= 0.002) discard_fragment();

    // Cylinder normal across the thread (screen space, Y down). Diffuse uses the fabric-space normal so a tilted
    // design keeps its brightness; the anisotropic sheen uses the tangent rotated into view space so the highlight
    // travels as the fabric tilts (screen Y is down, clip Y is up: flip, rotate by mvp's 3×3, flip back).
    float v = clamp(across / max(w, 1e-4), -1.0, 1.0);
    float3 N = normalize(float3(-in.dir.y * v, in.dir.x * v, sqrt(max(0.0, 1.0 - v * v))));
    float3 T = float3(in.dir, 0.0);
    float3x3 R = float3x3(u.mvp[0].xyz, u.mvp[1].xyz, u.mvp[2].xyz);
    T = normalize(R * float3(T.x, -T.y, T.z)); T.y = -T.y;
    float3 L = u.light;
    float3 V = float3(0, 0, 1);
    float diff = 0.62 + 0.38 * max(dot(N, L), 0.0);
    // Kajiya-Kay anisotropic highlight: strongest where the thread runs perpendicular to the light-view half vector.
    float3 H = normalize(L + V);
    float th = dot(T, H);
    float spec = pow(sqrt(max(0.0, 1.0 - th * th)), 48.0) * 0.55;
    // Subtle twist: fine periodic darkening along the thread (≈ 0.25 mm pitch) that fades out when zoomed out.
    float twist = 1.0 - 0.10 * smoothstep(1.5, 4.0, u.pxPerMM) * (0.5 + 0.5 * sin((along * 4.0 + across * 2.5) * 2.0 * M_PI_F));
    float3 rgb = in.color.rgb * diff * twist + spec;
    return float4(rgb * alpha, alpha);                                 // premultiplied for the blend state
}

// ---- Fabric background: fullscreen procedural weave ---------------------------------------------------------

struct FabricUniforms {     // matches SatinsmithRender.GPUFabricUniforms
    float4 tint;
    float2 sizePx;
    float  pxPerMM;
    int    kind;            // FabricPreset.code (1 linen, 2 twill, 3 knit, 4 felt, 5 satin)
    float2 originPx;        // pixel position of design (0,0) so the weave scrolls with pans
    float2 _pad;
    float4x4 mvp;           // tilt; the fabric is a large plane quad so it recedes with the design
};

struct FOut { float4 position [[position]]; float2 uv; };

vertex FOut fabric_vertex(uint vid [[vertex_id]], constant FabricUniforms &f [[buffer(0)]]) {
    // ±6 clip units: covers the screen flat and, tilted, reaches past the visible horizon. uv is the plane
    // position in flat clip space; Metal interpolates it perspective-correctly.
    const float E = 6.0;
    float2 p[6] = { {-E, -E}, {E, -E}, {-E, E},  {-E, E}, {E, -E}, {E, E} };
    FOut o; o.position = f.mvp * float4(p[vid], 0, 1); o.uv = p[vid] * 0.5 + 0.5; return o;
}

static inline float hash21(float2 p) { p = fract(p * float2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }

fragment float4 fabric_fragment(FOut in [[stage_in]], constant FabricUniforms &f [[buffer(0)]]) {
    float2 px = float2(in.uv.x, 1.0 - in.uv.y) * f.sizePx - f.originPx;   // pixels relative to design origin
    float2 mm = px / f.pxPerMM;
    float shade = 1.0;
    float noise = hash21(floor(px)) - 0.5;
    // Weave contrast fades out below ~3 px per mm: a 0.35 mm weave under 1 px per thread would only alias into moiré.
    // Measured from screen-space derivatives so the fade also follows the foreshortened far side of a tilted plane.
    float2 dmm = fwidth(mm);
    float effPxPerMM = 1.0 / max(max(dmm.x, dmm.y), 1e-5);
    float detail = smoothstep(1.5, 5.0, min(f.pxPerMM, effPxPerMM));
    switch (f.kind) {
        case 1: {   // linen: plain weave, ~0.35 mm threads
            float2 c = fract(mm / 0.35);
            float over = fmod(abs(floor(mm.x / 0.35) + floor(mm.y / 0.35)), 2.0);
            float warp = 1.0 - 0.18 * pow(abs(c.y * 2.0 - 1.0), 2.0);
            float weft = 1.0 - 0.18 * pow(abs(c.x * 2.0 - 1.0), 2.0);
            shade = (mix(warp, weft, over) + 0.06) * (1.0 + 0.05 * noise);     // averages ≈ 1.0 so the tint is the mean color
            break; }
        case 2: {   // twill: diagonal ribs 0.5 mm apart
            float rib = 0.5 + 0.5 * sin((mm.x + mm.y) / 0.5 * 2.0 * M_PI_F);
            shade = 0.93 + 0.14 * rib + 0.04 * noise;
            break; }
        case 3: {   // knit: vertical wales with horizontal course modulation
            float wale = 0.5 + 0.5 * cos(mm.x / 0.8 * 2.0 * M_PI_F);
            float course = 0.5 + 0.5 * cos(mm.y / 0.6 * 2.0 * M_PI_F + wale * 1.5);
            shade = 0.91 + 0.12 * wale + 0.06 * course + 0.03 * noise;
            break; }
        case 4: {   // felt: soft noise
            shade = 1.0 + 0.08 * noise + 0.03 * (hash21(floor(px / 3.0)) - 0.5);
            break; }
        case 5: {   // satin: smooth with slow sheen banding
            shade = 1.0 + 0.05 * sin(mm.x / 6.0) * cos(mm.y / 9.0) + 0.02 * noise;
            break; }
        default: shade = 1.0;
    }
    shade = mix(1.0 + 0.05 * noise, shade, detail);
    return float4(f.tint.rgb * shade, f.tint.a);
}
