#version 330 core

uniform vec3 iResolution;
uniform float iTime;
uniform float uAlpha;

out vec4 FragColor;

const vec3 LP = vec3(0.0, 0.0, 0.0);
const vec3 LC = vec3(0.85, 0.80, 0.70);
const vec3 HC1 = vec3(0.5, 0.4, 0.3);
const vec3 HC2 = vec3(0.1, 0.1, 0.6) * 0.5;
const vec3 HLD = vec3(0.0, 1.0, 0.0);
const vec3 BC = vec3(0.25, 0.25, 0.25);
const vec3 FC = vec3(1.30, 1.20, 1.00);

const float AS = 0.5;
const float DS = 1.0;
const float BS = 0.3;
const float FS = 0.3;

const float MAX_TRACE_DISTANCE = 50.0;
const float INTERSECTION_PRECISION = 0.0001;
const int NUM_OF_TRACE_STEPS = 256;
const float STEP_MULTIPLIER = 1.0;

#define PI 3.14159236
#define SCALE 2.5

struct Camera
{
    vec3 ro;
    vec3 rd;
    vec3 forward;
    vec3 right;
    vec3 up;
    float FOV;
};

struct Surface
{
    float len;
    vec3 position;
    vec3 colour;
    float id;
    float steps;
    float ao;
};

struct Model
{
    float dist;
    vec3 colour;
    float id;
};

vec2 toScreenspace(vec2 p)
{
    return (p - 0.5 * iResolution.xy) / min(iResolution.y, iResolution.x);
}

mat2 R(float a)
{
    float c = cos(a);
    float s = sin(a);
    return mat2(c, -s, s, c);
}

Camera getCamera(vec2 uv, vec3 pos, vec3 target)
{
    vec3 f = normalize(target - pos);
    vec3 r = normalize(vec3(f.z, 0.0, -f.x));
    vec3 u = normalize(cross(f, r));
    float FOV = 0.6;

    Camera cam;
    cam.ro = pos;
    cam.rd = normalize(f + FOV * uv.x * r + FOV * uv.y * u);
    cam.forward = f;
    cam.right = r;
    cam.up = u;
    cam.FOV = FOV;
    return cam;
}

vec2 fold(vec2 p, float a)
{
    p.x = abs(p.x);
    vec2 n = vec2(cos(a), sin(a));

    for (int i = 0; i < 2; ++i)
    {
        p -= 2.0 * min(0.0, dot(p, n)) * n;
        n = normalize(n - vec2(1.0, 0.0));
    }

    return p;
}

vec3 path(float delta)
{
    return vec3(
        cos(delta * 0.1) * 4.2 + sin(delta * 0.3) * 0.5 * cos(delta * 0.05),
        sin(delta * 0.04) * 5.4 + cos(delta * 0.04) * 5.4,
        delta
    );
}

Model model(vec3 p)
{
    float d = length(p) - 0.4;

    p.xy -= path(p.z).xy;

    float m = length(p.xy) * 0.5;
    float z = p.z;
    float r = cos(z * 0.2 + sin(m) * 0.3) * 0.4 + 0.5;
    float r2 = sin(z * 0.05124) * cos(z * 0.025203) + 1.0;

    p *= SCALE;

    vec3 q = p;

    p = vec3(R(0.05 * p.z + r) * p.xy, p.z);
    p.xy = fold(p.xy, PI / 6.0 + z * 0.2);
    p = mod(p, 3.0) - 1.5;

    vec3 o = abs(p);
    o -= (o.x + o.y + o.z) * 0.33333;

    float d0 = max(o.x, max(o.y, o.z)) - 0.01;
    float d1 = length(q.xy) - 1.0 - r2 * 2.0;

    d0 = max(d0, -d1);
    d0 = max(d0, length(q.xy) - 4.0 - r2 * 2.0);

    d = length(vec2(abs(d0), length(mod(p, vec3(0.1)) - 0.05))) - 0.3 * r;

    vec3 colour = mix(
        mix(
            vec3(0.8, 0.3, 0.6),
            vec3(0.3, 0.9, 0.9),
            vec3(
                cos(z * 0.1) * 0.5 + 0.5,
                sin(z * 0.12) * 0.5 + 0.5,
                cos(z * 0.05 + 1.0) * 0.5 + 0.5
            )
        ),
        vec3(1.0, 0.6, 0.4) * 0.1,
        m
    );

    Model outModel;
    outModel.dist = d / SCALE;
    outModel.colour = colour;
    outModel.id = 1.0;
    return outModel;
}

Model map(vec3 p)
{
    return model(p);
}

vec3 calcNormal(vec3 pos)
{
    vec3 eps = vec3(0.001, 0.0, 0.0);

    vec3 nor = vec3(
        map(pos + eps.xyy).dist - map(pos - eps.xyy).dist,
        map(pos + eps.yxy).dist - map(pos - eps.yxy).dist,
        map(pos + eps.yyx).dist - map(pos - eps.yyx).dist
    );

    return normalize(nor);
}

Surface march(Camera cam)
{
    float h = 1e4;
    float d = 0.0;
    float id = -1.0;
    float s = 0.0;
    float ao = 0.0;

    vec3 p = cam.ro;
    vec3 c = vec3(0.0);

    for (int i = 0; i < NUM_OF_TRACE_STEPS; i++)
    {
        if (abs(h) < INTERSECTION_PRECISION || d > MAX_TRACE_DISTANCE)
        {
            break;
        }

        p = cam.ro + cam.rd * d;

        Model m = map(p);

        h = m.dist;
        d += h * STEP_MULTIPLIER;
        id = m.id;
        s += 1.0;
        ao += max(h, 0.0);
        c = m.colour;
    }

    if (d >= MAX_TRACE_DISTANCE)
    {
        id = -1.0;
    }

    Surface outSurf;
    outSurf.len = d;
    outSurf.position = p;
    outSurf.colour = c;
    outSurf.id = id;
    outSurf.steps = s;
    outSurf.ao = ao;
    return outSurf;
}

float calcAO(vec3 pos, vec3 nor)
{
    float occ = 0.0;
    float sca = 1.0;

    for (int i = 0; i < 5; i++)
    {
        float hr = 0.01 + 0.12 * float(i) / 4.0;
        vec3 aopos = nor * hr + pos;
        float dd = map(aopos).dist;

        occ += -(dd - hr) * sca;
        sca *= 0.95;
    }

    return clamp(1.0 - 3.0 * occ, 0.0, 1.0);
}

vec3 shade(vec3 col, vec3 pos, vec3 nor, vec3 ref, Camera cam)
{
    vec3 plp = LP - pos;
    float o = calcAO(pos, nor);
    vec3 l = normalize(plp);

    float d = clamp(dot(nor, l), 0.0, 1.0) * DS;
    float b = clamp(dot(nor, normalize(vec3(-l.x, 0.0, -l.z))), 0.0, 1.0) * clamp(1.0 - pos.y, 0.0, 1.0) * BS;
    float f = pow(clamp(1.0 + dot(nor, cam.rd), 0.0, 1.0), 2.0) * FS;

    vec3 c = vec3(0.0);
    c += d * LC;
    c += mix(HC1, HC2, dot(nor, HLD)) * AS;
    c += b * BC * o;
    c += f * FC * o;

    return col * c;
}

vec3 render(Surface surface, Camera cam, vec2 uv)
{
    vec3 colour = vec3(0.1, 0.0, 0.3);
    vec3 colourB = vec3(0.1, 0.05, 0.2);

    colour = mix(colourB, colour, pow(length(uv), 2.0) / 1.5);

    vec3 bg = colour;

    if (surface.id > 0.0)
    {
        vec3 surfaceNormal = calcNormal(surface.position);
        vec3 ref = reflect(cam.rd, surfaceNormal);
        colour = shade(surface.colour, surface.position, surfaceNormal, ref, cam);
    }

    float sceneLength = length(cam.ro - surface.position);
    float fog = smoothstep(MAX_TRACE_DISTANCE, -3.0, sceneLength);

    colour = mix(bg, colour, pow(fog, 2.0));
    colour *= clamp(1.0 / max(surface.steps * 0.02, 0.001), 0.2, 10.0);

    return colour;
}

void mainImage(out vec4 c, in vec2 f)
{
    vec2 uv = toScreenspace(f.xy);

    float t = iTime * 5.0;

    vec3 la = path(t + 0.5);
    Camera cam = getCamera(uv, path(t), la);

    vec2 a = sin(vec2(1.5707963, 0.0) - path(la.z).x / 8.0);
    mat2 rM = mat2(a.x, -a.y, a.y, a.x);

    cam.rd.xy = rM * cam.rd.xy;

    Surface surface = march(cam);

    vec3 r = render(surface, cam, uv);

    vec4 c2 = vec4(r, 1.0);

    c = clamp(
        mix(
            c2,
            c2 * 2.0,
            clamp(surface.len * 0.05, 0.0, 1.0)
        ),
        vec4(0.0),
        vec4(1.0)
    );
}

void main()
{
    vec4 col = vec4(0.0);
    mainImage(col, gl_FragCoord.xy);
    col.a *= uAlpha;
    FragColor = col;
}
