#version 330 core
out vec4 FragColor;

uniform vec3 iResolution;
uniform float iTime;
uniform float uAlpha;
uniform sampler2D iChannel0;

#define FAR 40.0
#define scale 1.5
#define SHOW_BALL_JOINS
#define SHOW_HORIZONTAL
#define SHOW_VERTICAL
#define BLINKING_LIGHTS

vec3 vObjID;
vec3 hVal;
float diag;

mat2 r2(float th)
{
    vec2 a = sin(vec2(1.5707963, 0.0) + th);
    return mat2(a, -a.y, a.x);
}

float hash21(vec2 p)
{
    float n = dot(p, vec2(13.163, 157.247));
    return fract(sin(n) * 43758.5453);
}

float sdCapsule(vec3 p, vec3 a, vec3 b)
{
    vec3 pa = p - a;
    vec3 ba = b - a;
    float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    pa = abs(pa - ba * h);
    return length(pa);
}

float hm(vec2 p)
{
    p = p / 4.0 + iTime / 8.0;
    float n = dot(sin(p.xy * 3.14159 - cos(p.yx * 3.14159) * 3.14159 / 2.0), vec2(0.25) + 0.5) * 0.66;
    p *= 1.5;
    p.xy = mat2(0.866025, 0.5, -0.5, 0.866025) * p.xy;
    n += dot(sin(p.xy * 3.14159 - cos(p.yx * 3.14159) * 3.14159 / 2.0), vec2(0.25) + 0.5) * 0.34;
    return n;
}

float triTerrain(vec2 p)
{
    vec2 ip = floor(p);
    p -= ip;

    float h00 = hm(ip);
    float h01 = hm(ip + vec2(0.0, 1.0));
    float h10 = hm(ip + vec2(1.0, 0.0));
    float h11 = hm(ip + 1.0);

    float l1 = abs(h00 - h11);
    float l2 = abs(h01 - h10);
    diag = l1 > l2 ? 0.0 : 1.0;

    float s;
    if (diag < 0.5)
    {
        s = step(1.0, p.x + p.y);
        float hs = s < 0.5 ? h00 : h11;
        hVal = vec3(hs, h10, h01);
        return mix(mix(h00, h10, p.x), mix(h01, h11, p.x), p.y);
    }
    else
    {
        s = step(p.x, p.y);
        float hs = s < 0.5 ? h10 : h01;
        hVal = vec3(h00, hs, h11);
        return mix(mix(h00, h01, p.y), mix(h10, h11, p.y), p.x);
    }
}

float map(vec3 p)
{
    float ter = triTerrain(p.xz * scale) / scale;
    const float hPert = 0.4;
    float fl = p.y + (0.5 - ter) * hPert;

    hVal = (hVal - 0.5 * scale) * hPert + 0.04;

    vec3 q = p * scale;
    q.xz = fract(q.xz);

    const float lw = 0.04;
    float ln = 1e5;
    float ln2 = 1e5;

    if (diag < 0.5)
    {
        ln = min(ln, sdCapsule(q, vec3(0.0, hVal.x, 0.0), vec3(1.0, hVal.y, 0.0)));
        ln = min(ln, sdCapsule(q, vec3(0.0, hVal.z, 1.0), vec3(1.0, hVal.x, 1.0)));
        ln = min(ln, sdCapsule(q, vec3(0.0, hVal.x, 0.0), vec3(0.0, hVal.z, 1.0)));
        ln = min(ln, sdCapsule(q, vec3(1.0, hVal.y, 0.0), vec3(1.0, hVal.x, 1.0)));
        ln -= lw;
        ln2 = min(ln2, sdCapsule(q, vec3(0.0, hVal.z, 1.0), vec3(1.0, hVal.y, 0.0)) - lw);
    }
    else
    {
        ln = min(ln, sdCapsule(q, vec3(0.0, hVal.x, 0.0), vec3(1.0, hVal.y, 0.0)));
        ln = min(ln, sdCapsule(q, vec3(0.0, hVal.y, 1.0), vec3(1.0, hVal.z, 1.0)));
        ln = min(ln, sdCapsule(q, vec3(0.0, hVal.x, 0.0), vec3(0.0, hVal.y, 1.0)));
        ln = min(ln, sdCapsule(q, vec3(1.0, hVal.y, 0.0), vec3(1.0, hVal.z, 1.0)));
        ln -= lw;
        ln2 = min(ln2, sdCapsule(q, vec3(0.0, hVal.x, 0.0), vec3(1.0, hVal.z, 1.0)) - lw);
    }

    float sp = 1e5;
    #ifdef SHOW_BALL_JOINS
    float hgt = (hm(floor(p.xz * scale + 0.5)) - 0.5 * scale) * hPert + 0.04;
    vec2 offXZ = fract(p.xz * scale + 0.5) - 0.5;
    sp = length(vec3(offXZ.x, abs(q.y - hgt), offXZ.y)) - 0.09;
    #endif

    ln /= scale;
    ln2 /= scale;
    sp /= scale;

    vObjID = vec3(fl, ln, sp);
    return min(min(fl, ln), sp) * 0.75;
}

float trace(vec3 ro, vec3 rd)
{
    float t = 0.0;
    for (int i = 0; i < 96; i++)
    {
        float d = map(ro + rd * t);
        if (abs(d) < 0.001 * (t * 0.04 + 1.0) || t > FAR)
            break;
        t += d;
    }
    return min(t, FAR);
}

vec3 getNormal(in vec3 p)
{
    const vec2 e = vec2(0.002, 0.0);
    return normalize(vec3(
        map(p + e.xyy) - map(p - e.xyy),
        map(p + e.yxy) - map(p - e.yxy),
        map(p + e.yyx) - map(p - e.yyx)));
}

float getShad(in vec3 ro, in vec3 n, in vec3 lp)
{
    const float eps = 0.001;
    float t = 0.0;
    float sh = 1.0;
    ro += n * eps * 1.1;
    vec3 ld = (lp - ro);
    float lDist = length(ld);
    ld /= lDist;

    for (int i = 0; i < 16; i++)
    {
        float dt = map(ro + ld * t);
        sh = min(sh, 12.0 * dt / max(t, 0.001));
        t += clamp(dt, 0.01, 0.5);
        if (dt < 0.0 || t > lDist)
            break;
    }

    return min(max(sh, 0.0) + 0.2, 1.0);
}

float cAO(in vec3 pos, in vec3 nor)
{
    float sca = 2.0, occ = 0.0;
    for (int i = 0; i < 5; i++)
    {
        float hr = 0.01 + float(i) * 0.5 / 4.0;
        float dd = map(nor * hr + pos);
        occ += (hr - dd) * sca;
        sca *= 0.7;
    }
    return clamp(1.0 - occ, 0.0, 1.0);
}

vec3 tex3D(sampler2D channel, vec3 p, vec3 n)
{
    n = max(abs(n) - 0.2, 0.001);
    n /= dot(n, vec3(1.0));
    vec3 tx = texture(channel, p.zy).xyz;
    vec3 ty = texture(channel, p.xz).xyz;
    vec3 tz = texture(channel, p.xy).xyz;
    return tx * tx * n.x + ty * ty * n.y + tz * tz * n.z;
}

float n3D(in vec3 p)
{
    const vec3 s = vec3(57.0, 113.0, 27.0);
    vec3 ip = floor(p);
    p -= ip;
    vec4 h = vec4(0.0, s.yz, s.y + s.z) + dot(ip, s);
    p = p * p * (3.0 - 2.0 * p);
    h = mix(fract(sin(h) * 43758.5453), fract(sin(h + s.x) * 43758.5453), p.x);
    h.xy = mix(h.xz, h.yw, p.y);
    return mix(h.x, h.y, p.z);
}

vec3 envMap(vec3 p)
{
    p *= 2.0;
    p.xz += iTime * 0.5;
    float n3D2 = n3D(p * 2.0);
    float c = n3D(p) * 0.57 + n3D2 * 0.28 + n3D(p * 4.0) * 0.15;
    c = smoothstep(0.5, 1.0, c);
    p = vec3(c * 0.8, c * 0.9, c);
    return mix(p.zxy, p, n3D2 * 0.34 + 0.665);
}

vec2 path(float z)
{
    float s = sin(z / 36.0) * cos(z / 18.0);
    return vec2(s * 16.0, 0.0);
}

float lightBlink(vec2 p)
{
    float rnd = hash21(floor(p));
    return smoothstep(0.9, 1.0, sin(rnd * 6.283 + iTime * 2.0) * 0.5 + 0.5);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 uv = (fragCoord - iResolution.xy * 0.5) / min(iResolution.y, 800.0);
    vec3 lk = vec3(0.0, 0.0, iTime * 1.5);
    vec3 ro = lk + vec3(0.0, 2.5, -2.0);
    vec3 lp = ro + vec3(0.0, 0.0, 3.0);

    lk.xy += path(lk.z);
    ro.xy += path(ro.z);
    lp.xy += path(lp.z);

    float FOV = 3.14159 / 3.0;
    vec3 fwd = normalize(lk - ro);
    vec3 rgt = normalize(vec3(fwd.z, 0.0, -fwd.x));
    vec3 up = cross(fwd, rgt);
    vec3 rd = normalize(fwd + (uv.x * rgt + uv.y * up) * FOV);
    rd.xy *= r2(path(lk.z).x / 128.0);

    float t = trace(ro, rd);
    float svObjID = vObjID.x < vObjID.y ? (vObjID.x < vObjID.z ? 0.0 : 2.0) : (vObjID.y < vObjID.z ? 1.0 : 2.0);
    vec3 sceneCol = vec3(0.0);

    if (t < FAR)
    {
        vec3 sp = ro + rd * t;
        vec3 sn = getNormal(sp);
        vec3 ld = lp - sp;
        float lDist = max(length(ld), 0.001);
        ld /= lDist;
        float sh = getShad(sp, sn, lp);
        float ao = cAO(sp, sn);
        float atten = 1.2 / (1.0 + lDist * lDist * 0.05);
        float diff = pow(max(dot(sn, ld), 0.0), 4.0) * 1.5;
        float spec = pow(max(dot(reflect(-ld, sn), -rd), 0.0), 8.0);
        float fre = pow(clamp(dot(sn, rd) + 1.0, 0.0, 1.0), 4.0);
        vec3 ref = reflect(rd, sn);
        vec3 env = envMap(ref);
        vec3 specCol = vec3(1.0, 0.95, 0.8);
        vec3 texCol;

        if (svObjID < 0.5)
        {
            texCol = tex3D(iChannel0, sp, sn);
            texCol = smoothstep(-0.07, 0.7, texCol);
            texCol = mix(texCol, mix(texCol, texCol.yyz, 0.35) * 3.5, lightBlink(sp.xz * scale));
            specCol *= (n3D(sp * 96.0) * 0.66 + n3D(sp * 192.0) * 0.34) * 0.3 + 0.35;
            env *= texCol / 1.5;
            texCol = mix(texCol, texCol.xzy, dot(ref, vec3(0.15)));
        }
        else
        {
            texCol = tex3D(iChannel0, sp * 2.0, sn);
            texCol = mix(texCol, texCol.xxx, 0.75) * 0.35;
            if (svObjID == 2.0)
                env *= vec3(2.2, 1.7, 0.5);
        }

        sceneCol += (texCol * (diff + ao * 0.3 + vec3(1.0, 0.9, 0.7) * fre) + env * 1.5 + specCol * spec * 2.0);
        sceneCol *= ao * atten * sh;
    }

    vec3 bg = mix(vec3(1.0, 0.5, 0.6), vec3(0.1, 0.05, 0.025), clamp(rd.y + 0.75, 0.0, 1.0));
    bg = mix(bg, bg.xzy, rd.y * 0.5 + 0.5);
    sceneCol = mix(sceneCol, bg, smoothstep(0.0, 0.95, t / FAR));

    fragColor = vec4(sqrt(clamp(sceneCol, 0.0, 1.0)), uAlpha);
}

void main()
{
    mainImage(FragColor, gl_FragCoord.xy);
}
