#include <metal_stdlib>
using namespace metal;

static float sp_saturate(float value) {
    return clamp(value, 0.0, 1.0);
}

static float sp_smoothstep(float edge0, float edge1, float value) {
    float t = sp_saturate((value - edge0) / (edge1 - edge0));
    return t * t * (3.0 - 2.0 * t);
}

static float3 sp_mix(float3 a, float3 b, float t) {
    return a + (b - a) * t;
}

static float sp_edgeFade(float fx) {
    return sp_smoothstep(0.0, 0.24, fx) * (1.0 - sp_smoothstep(0.76, 1.0, fx));
}

static float2 sp_normalizedPosition(float2 position, float2 size) {
    if (max(abs(position.x), abs(position.y)) <= 2.0) {
        return clamp(position, float2(0.0), float2(1.0));
    }
    return clamp(position / max(size, float2(1.0)), float2(0.0), float2(1.0));
}

static float sp_radialStop(float distance, float alpha0, float alpha1, float alpha2) {
    float d = sp_saturate(distance);
    if (d < 0.5) {
        return mix(alpha0, alpha1, d / 0.5);
    }
    return mix(alpha1, alpha2, (d - 0.5) / 0.5);
}

static float sp_strokeCoverage(float distanceY, float lineWidth, float antialiasWidth) {
    float halfWidth = lineWidth * 0.5;
    return 1.0 - sp_smoothstep(halfWidth, halfWidth + antialiasWidth, distanceY);
}

static float sp_blurredStrokeCoverage(float distanceY, float lineWidth, float blurRadius, float antialiasWidth) {
    float outsideStroke = max(0.0, distanceY - lineWidth * 0.5);
    float sigma = max(antialiasWidth, blurRadius * 0.46);
    return exp(-(outsideStroke * outsideStroke) / (2.0 * sigma * sigma));
}

static float2 sp_waveCoverage(float2 uv,
                              float viewHeight,
                              float time,
                              float totalAmplitude,
                              float frequency,
                              float speed,
                              float waveAmplitude,
                              float phase,
                              float baseLineWidth,
                              float widthScale,
                              float drive,
                              float hero) {
    constexpr float pi = 3.14159265358979323846;
    float fx = sp_saturate(uv.x);
    float centerY = 0.5;
    float envelope = pow(max(0.0, sin(pi * fx)), 1.2);

    // Canvas applies a damped speech contour that only nudges the ribbon body
    // from 0.9 to roughly 1.0. The shader receives the smoothed voice energy,
    // so keep the same small range without adding unrelated procedural wiggle.
    float body = 0.9 + 0.08 * drive;
    float osc = sin(2.0 * pi * frequency * fx + time * speed + phase);
    float y = centerY + envelope * waveAmplitude * osc * body * totalAmplitude;
    float distanceY = abs(uv.y - y);

    float fade = sp_edgeFade(fx);
    float pointToUnit = 1.0 / max(viewHeight, 1.0);
    float bloomWidth = baseLineWidth * widthScale * (1.55 + 1.10 * drive) * pointToUnit;
    float crispWidth = baseLineWidth * widthScale * (0.86 + 0.38 * drive) * pointToUnit;
    float bloomBlur = mix(5.5, 13.0, hero) * pointToUnit;
    float crispBlur = mix(0.55, 0.75, hero) * pointToUnit;
    float antialiasWidth = 1.35 * pointToUnit;
    float bloom = sp_blurredStrokeCoverage(distanceY, bloomWidth, bloomBlur, antialiasWidth) * fade;
    float crisp = sp_blurredStrokeCoverage(distanceY, crispWidth * 0.72, crispBlur, antialiasWidth) * fade;
    return float2(crisp, bloom);
}

static float3 sp_applyWave(float3 base,
                           float3 waveColor,
                           float2 coverage,
                           float dark,
                           float drive,
                           float breath,
                           float core) {
    float bloomOpacity = mix(0.20, 0.42, dark);
    float crispLayerOpacity = mix(0.34, 0.78, dark) * (0.5 + 0.5 * drive + 0.1 * breath);
    float bloomAlpha = sp_saturate(coverage.y * bloomOpacity);
    float crispAlpha = sp_saturate(coverage.x * crispLayerOpacity);

    if (dark > 0.5) {
        base += waveColor * bloomAlpha;
        base += waveColor * crispAlpha;
    } else {
        base = sp_mix(base, waveColor, bloomAlpha);
        base = sp_mix(base, waveColor, crispAlpha);
    }
    return base;
}

static half4 sp_renderVoiceLightBackdrop(float2 uv,
                                          float3 accent,
                                          float2 size,
                                          float time,
                                          float energy,
                                          float isDark,
                                          float isHero,
                                          float isActive) {
    float2 safeSize = max(size, float2(1.0, 1.0));
    uv = clamp(uv, float2(0.0), float2(1.0));
    float dark = step(0.5, isDark);
    float hero = step(0.5, isHero);
    float clampedEnergy = sp_saturate(energy);
    float drive = pow(clampedEnergy, 0.72);
    float visualDrive = drive;
    float breath = sin(time * 0.9) * 0.5 + 0.5;

    // Canvas base gradient.
    float3 darkTop = float3(0.060, 0.070, 0.120);
    float3 darkBottom = float3(0.030, 0.035, 0.065);
    float3 lightTop = float3(0.970, 0.980, 1.000);
    float3 lightBottom = float3(0.900, 0.920, 0.980);
    float3 baseDark = sp_mix(darkTop, darkBottom, uv.y);
    float3 baseLight = sp_mix(lightTop, lightBottom, uv.y);
    float3 color = sp_mix(baseLight, baseDark, dark);

    // Canvas center glow.
    float idle = 0.14 + 0.10 * breath;
    float intensity = min(1.0, idle + clampedEnergy);
    float2 center = float2(0.5, 0.5);
    float radius = max(safeSize.x, safeSize.y) * 0.55;
    float radialDistance = length((uv - center) * safeSize) / max(radius, 1.0);
    if (dark > 0.5) {
        float centerAlpha = 0.16 * (0.6 + 0.4 * intensity);
        float glowAlpha = sp_radialStop(radialDistance, centerAlpha, 0.05, 0.0);
        color += float3(0.28, 0.42, 0.95) * glowAlpha;
    } else {
        float glowAlpha = mix(0.06 * (0.6 + 0.4 * intensity), 0.0, sp_saturate(radialDistance));
        color = sp_mix(color, float3(0.40, 0.55, 1.0), glowAlpha);
    }

    // Canvas halo: full-width vertical accent wash, not a narrow center beam.
    float centerY = 0.5;
    float haloHeight = mix(0.68, 0.60, hero);
    float haloDistance = abs(uv.y - centerY) / max(haloHeight * 0.5, 0.001);
    float haloShape = pow(sp_saturate(1.0 - haloDistance), 1.35);
    float haloOpacity = mix(0.055, 0.070, dark) * (0.40 + 0.45 * visualDrive + 0.10 * (sin(time * 0.8) * 0.5 + 0.5));
    float haloAlpha = haloShape * haloOpacity;
    if (dark > 0.5) {
        color += accent * haloAlpha * 0.45;
    } else {
        color = sp_mix(color, accent, haloAlpha);
    }

    // Canvas waveform amplitude and animation constants.
    float waveBreath = sin(time * 0.8) * 0.5 + 0.5;
    float idleAmp = mix(0.03, 0.012, hero) * (0.6 + 0.8 * waveBreath);
    float activeAmp = mix(0.26, 0.20, hero);
    float totalAmplitude = idleAmp + visualDrive * activeAmp;
    float widthScale = mix(0.82, 1.35, hero);

    float2 core = sp_waveCoverage(uv, safeSize.y, time, totalAmplitude,
                                  1.50, 0.45, 1.00, 0.0, 2.10, widthScale, visualDrive, hero);
    color = sp_applyWave(color, sp_mix(accent, float3(1.0, 1.0, 1.0), dark), core, dark, visualDrive, waveBreath, 1.0);

    float2 blue = sp_waveCoverage(uv, safeSize.y, time, totalAmplitude,
                                  1.70, -0.50, 0.86, 1.3, 1.80, widthScale, visualDrive, hero);
    color = sp_applyWave(color, float3(0.34, 0.55, 1.00), blue, dark, visualDrive, waveBreath, 0.0);

    float2 teal = sp_waveCoverage(uv, safeSize.y, time, totalAmplitude,
                                  1.60, -0.38, 0.80, 4.0, 1.70, widthScale, visualDrive, hero);
    color = sp_applyWave(color, float3(0.20, 0.84, 0.80), teal, dark, visualDrive, waveBreath, 0.0);

    float2 magenta = sp_waveCoverage(uv, safeSize.y, time, totalAmplitude,
                                     1.90, 0.60, 0.68, 2.7, 1.60, widthScale, visualDrive, hero);
    color = sp_applyWave(color, float3(0.85, 0.34, 0.80), magenta, dark, visualDrive, waveBreath, 0.0);

    // Canvas vignette.
    float vignetteRadius = max(safeSize.x, safeSize.y) * 0.72;
    float vignetteDistance = length((uv - center) * safeSize) / max(vignetteRadius, 1.0);
    float vignetteAlpha = sp_smoothstep(0.55, 1.0, vignetteDistance) * mix(0.06, 0.34, dark);
    color *= 1.0 - vignetteAlpha;

    return half4(half3(sp_saturate(color.r), sp_saturate(color.g), sp_saturate(color.b)), 1.0h);
}

[[ stitchable ]] half4 voiceLightBackdrop(float2 position,
                                          half4 sourceColor,
                                          float2 size,
                                          float time,
                                          float energy,
                                          float isDark,
                                          float isHero,
                                          float isActive) {
    float2 safeSize = max(size, float2(1.0, 1.0));
    float2 uv = sp_normalizedPosition(position, safeSize);
    return sp_renderVoiceLightBackdrop(uv,
                                       float3(sourceColor.rgb),
                                       safeSize,
                                       time,
                                       energy,
                                       isDark,
                                       isHero,
                                       isActive);
}

struct VoiceLightBackdropUniforms {
    float2 size;
    float time;
    float energy;
    float isDark;
    float isHero;
    float isActive;
    float padding0;
    float4 accent;
};

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

vertex VoiceLightBackdropVertexOut voiceLightBackdropVertex(uint vertexID [[vertex_id]]) {
    constexpr float2 positions[3] = {
        float2(-1.0, -1.0),
        float2(3.0, -1.0),
        float2(-1.0, 3.0),
    };
    constexpr float2 uvs[3] = {
        float2(0.0, 1.0),
        float2(2.0, 1.0),
        float2(0.0, -1.0),
    };

    VoiceLightBackdropVertexOut out;
    out.position = float4(positions[vertexID], 0.0, 1.0);
    out.uv = uvs[vertexID];
    return out;
}

fragment half4 voiceLightBackdropFragment(VoiceLightBackdropVertexOut in [[stage_in]],
                                          constant VoiceLightBackdropUniforms &uniforms [[buffer(0)]]) {
    return sp_renderVoiceLightBackdrop(in.uv,
                                       uniforms.accent.rgb,
                                       uniforms.size,
                                       uniforms.time,
                                       uniforms.energy,
                                       uniforms.isDark,
                                       uniforms.isHero,
                                       uniforms.isActive);
}
