#version 410 core

// Using the same uniform names as ShaderToy for compatibility

uniform vec3 iResolution;
uniform float iGlobalTime;

vec4 iMouse = vec4(iResolution.xy / 2.0, 0.0, 0.0); // TODO

// ---- Shadertoy shader starts here --------------------------------------

// Time to break shadertoy compatibility :)
uniform sampler2D bgTex;
uniform sampler2D envTex;
uniform sampler2D scrollTex;

// Thanks to:
//  iq for the noise routine and some repetition stuff
//  las (and the rest of mercury) for help with reducing discontinuities

vec3 hableTonemap(vec3 x)
{
    float a = .15;
    float b = .5;
    float c = .1;
    float d = .2;
    float e = .02;
    float f = .3;

    return ((x * (x * a + c * b) + d * e) / (x * (x * a + b) + d * f)) - e / f;
}

vec3 tonemap(vec3 rawColor, float exposure)
{
    float w = 11.2;

    vec3 exposedColor = max(rawColor * exposure, 0.0);

    vec3 linear = pow(exposedColor, vec3(1.0 / 2.2));

    vec3 reinhard = pow(exposedColor / (exposedColor + 1.0), vec3(1.0 / 2.2));

    vec3 x = max(exposedColor - .004, 0.0);
    vec3 hejlBurgessDawson = (x * (x * 6.2 + .5)) / (x * (x * 6.2 + 1.7) + .06);

    float exposureBias = 2.0;
    vec3 curr = hableTonemap(exposedColor * exposureBias);
    float whiteScale = (vec3(1.0) / hableTonemap(vec3(w))).x;
    vec3 color = curr * whiteScale;
    vec3 hable = pow(color, vec3(1.0 / 2.2));

    return
        //linear
        //reinhard
        hejlBurgessDawson
        //hable
        ;
}

struct Material
{
    vec3 additiveColor;
    vec3 diffuseColor;
    vec3 specularColor;
    float specularExponent;
    float fresnelAmount;
};

Material mixMaterials(Material a, Material b, float x)
{
    return Material(mix(a.additiveColor, b.additiveColor, x),
                    mix(a.diffuseColor, b.diffuseColor, x),
                    mix(a.specularColor, b.specularColor, x),
                    mix(a.specularExponent, b.specularExponent, x),
                    mix(a.fresnelAmount, b.fresnelAmount, x));
}

struct SceneResult
{
    float d;
    Material material;
};

vec3 rotateX(vec3 x, float an)
{
    float c = cos(an);
    float s = sin(an);
    return vec3(x.x, x.y * c - x.z * s, x.z * c + x.y * s);
}

vec3 rotateY(vec3 x, float an)
{
    float c = cos(an);
    float s = sin(an);
    return vec3(x.x * c - x.z * s, x.y, x.z * c + x.x * s);
}

vec3 rotateZ(vec3 x, float an)
{
    float c = cos(an);
    float s = sin(an);
    return vec3(x.x * c - x.y * s, x.y * c + x.x * s, x.z);
}

// Noise by iq
float hash( float n ) { return fract(sin(n)*753.5453123); }
float noise( in vec3 x )
{
    vec3 p = floor(x);
    vec3 f = fract(x);
    f = f*f*(3.0-2.0*f);

    float n = p.x + p.y*157.0 + 113.0*p.z;
    return mix(mix(mix( hash(n+  0.0), hash(n+  1.0),f.x),
                   mix( hash(n+157.0), hash(n+158.0),f.x),f.y),
               mix(mix( hash(n+113.0), hash(n+114.0),f.x),
                   mix( hash(n+270.0), hash(n+271.0),f.x),f.y),f.z);
}

float fbm(vec3 x)
{
    float ret = noise(x);
    ret += noise(x * 2.0) / 2.0;
    ret += noise(x * 4.0) / 4.0;
    ret += noise(x * 8.0) / 8.0;
    ret += noise(x * 16.0) / 16.0;
    return ret;
}

#define saturate(x) (clamp(x, 0.0, 1.0))

float sphere(vec3 p, vec3 pos, float radius)
{
    return length(p - pos) - radius;
}

float cube(vec3 p, vec3 pos, vec3 size)
{
    vec3 d = abs(p - pos) - size;
    return max(max(d.x, d.y), d.z);
}

SceneResult f(vec3 p)
{
    vec3 op = p;

    float t = iGlobalTime * .12;

    float preRotX = cos(t * .133) * .1;
    float preRotY = sin(t * .87) * .1;
    float preRotZ = cos(t * .383) * .1;

    //p = rotateZ(p, preRotZ);
    p = rotateX(p, preRotX);
    //p = rotateY(p, preRotY);

    p = vec3(abs(p.x) - (sin(t * .112) * .5 + .5) * 2.6, p.y, abs(p.z));

    float rotX = cos(t * .387);
    float rotY = sin(t * .1);
    float rotZ = sin(t * .083);

    p = rotateZ(p, rotZ);
    p = rotateX(p, rotX);
    p = rotateY(p, rotY);

    vec3 cubeP = op;
    cubeP = rotateZ(cubeP, rotZ);
    cubeP = rotateX(cubeP, rotX);
    cubeP = rotateY(cubeP, rotY);

    Material material = Material(vec3(0.0),
                                 vec3(.3, .1, 1.0) * .2,
                                 vec3(0.0),
                                 36.0,
                                 .8);

    float slashesAmt = sin(t * 1.433) * .1 + .3;

    float slashes = abs(fract((p.x + p.y + p.z) / 2.0) - 1.0 / 2.0) - slashesAmt / 2.0;
    float d = mix(max(max(max(max(cube(p, vec3(0.0), vec3(3.7)),
                                  sphere(p,
                                         vec3(cos(t * .42),
                                              cos(t * .777),
                                              sin(t * .588) - .3) * 1.1,
                                         5.0)),
                              -sphere(p,
                                      vec3(sin(t * .82),
                                           sin(t),
                                           cos(t * .33) - .3) * 1.1,
                                      4.0)),
                          slashes),
                      cube(op, vec3(0.0), vec3(8.0, 4.0, 4.0) * clamp((iGlobalTime - 2.0) / 30.0, 0.0, 1.0))),
                  cube(cubeP,
                       vec3(0.0),
                       vec3(3.7)),
                  smoothstep(0.0, 1.0, clamp((iGlobalTime - 60.0) / 30.0, 0.0, 1.0)));

    return SceneResult(d, material);
}

vec3 lightContribution(vec3 eyePos,
                       vec3 eyeDir,
                       vec3 normal,
                       vec3 lightPos,
                       vec3 lightColor,
                       Material material,
                       float occlusionTerm)
{
    vec3 l = normalize(lightPos - eyePos);
    vec3 diffuse = max(material.diffuseColor * (max(dot(normal, l), 0.0)) - occlusionTerm, vec3(0.0));
    vec3 refVec = reflect(l, normal);
    vec3 specular = material.specularColor * pow(max(dot(refVec, eyeDir), 0.0), material.specularExponent);
    return (diffuse + specular) * lightColor;
}

float lightTime = iGlobalTime * .2;
vec3 light1Pos = normalize(vec3(sin(lightTime * 1.1), cos(lightTime) * .3 + .4, sin(lightTime * .76) - 1.1)) * 20.0;
vec3 light2Pos = normalize(vec3(cos(lightTime * .84), cos(lightTime * .45) * .3 + .4, sin(lightTime * 1.2) - 1.1)) * 20.0;
vec3 light1Color = vec3(0.6, .8, 1.0);
vec3 light2Color = vec3(1.0, .8, .6);

vec3 getColor(float x)
{
    return
        mix(
            vec3(.2, .8, 1.0),
            mix(
                vec3(.7, .2, 1.0),
                mix(
                    vec3(1.0, .6, .2),
                    vec3(1.0),
                    step(.6, x)),
                step(.4, x)),
            step(.2, x));
}

vec3 traceSphere(vec3 center, float radius, vec3 eyePos, vec3 eyeDir)
{
    vec3 l = center - eyePos;
    float tca = dot(l, eyeDir);
    float d2 = dot(l, l) - tca * tca;
    float thc = sqrt(radius * radius - d2);
    float t = tca + thc;
    return eyePos + eyeDir * t;
}

vec3 forest(sampler2D sampler, vec3 eyePos, vec3 eyeDir)
{
    vec3 intersectionPoint = traceSphere(vec3(0.0), 50.0, eyePos, eyeDir);
    vec3 normal = normalize(intersectionPoint - eyePos);
    float u = (atan(normal.z, normal.x) / 2.0 / 3.141592) - .1;
    float v = -(asin(normal.y) + 3.141592 / 2.0) / 3.141592;
    return pow(texture(sampler, vec2(u, v)).rgb, vec3(2.2)) * 2.0;
}

vec3 background(vec3 eyePos, vec3 eyeDir)
{
    return
        forest(bgTex, eyePos, eyeDir)
        ;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 pixelPos = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;

    float fovDegrees = 60.0;
    float radius = length(pixelPos);
    float theta = radius * 3.141592 * fovDegrees / 360.0;
    vec3 eyeDir = normalize(vec3(pixelPos.x / radius * sin(theta), pixelPos.y / radius * sin(theta), cos(theta)));

    //vec3 eyeDir = normalize(vec3(pixelPos, 2.0));

    float camRotX = sin(iGlobalTime * .1) * .01 + fbm(vec3(iGlobalTime * .02) + vec3(.2, 0.0, 0.0)) * .03;
    float camRotY = cos(iGlobalTime * .387) * .01 + fbm(vec3(iGlobalTime * .0477) + vec3(0.0, .4, 0.0)) * .03;
    float camRotZ = sin(iGlobalTime * .083) * .01 + fbm(vec3(iGlobalTime * .0623) + vec3(0.0, 0.0, -.3)) * .03;

    eyeDir = rotateZ(eyeDir, camRotZ);
    eyeDir = rotateX(eyeDir, camRotX);
    eyeDir = rotateY(eyeDir, camRotY);

    vec3 eyePos = vec3(0.0, 0.0, -20.0) + fbm(vec3(iGlobalTime * .01)) * .1;
    eyePos = rotateX(eyePos, camRotX);
    eyePos = rotateY(eyePos, camRotY);

    vec3 color = background(eyePos, eyeDir);

    const float maxDisTravelled = 40.0;
    float disTravelled = 0.0;
    const int maxIterations = 160;
    SceneResult originalIntersection;
    for (int i = 0; i < maxIterations; i++)
    {
        originalIntersection = f(eyePos);
        float d = f(eyePos).d;
        if (d <= 0.0)
            break;

        d = max(d, .025);
        eyePos += eyeDir * d;
        disTravelled += d;
        if (disTravelled >= maxDisTravelled)
            break;
    }

    if (disTravelled < maxDisTravelled)
    {
        const int discontinuityReductionIterations = 5;
        for (int i = 0; i < discontinuityReductionIterations; i++)
        {
            if (originalIntersection.d >= 0.0)
                break;

            eyePos += eyeDir * originalIntersection.d;
            originalIntersection = f(eyePos);
        }

        SceneResult td = originalIntersection;
        vec3 normal = normalize(vec3(f(eyePos + vec3(.003, 0, 0)).d - td.d,
                                     f(eyePos + vec3(0, .003, 0)).d - td.d,
                                     f(eyePos + vec3(0, 0, .003)).d - td.d));

        float occlusionTerm = 0.0;
        for (float i = 1.0; i < 4.0; i += 1.0)
            occlusionTerm += max(-f(eyePos + normal * i * .1).d, 0.0) / pow(2.0, i);
        //occlusionTerm *= 2.0;

        float fresnelApproximation = pow(1.0 - saturate(-dot(eyeDir, normal)), 8.0);
        vec3 reflectionVector = reflect(eyeDir, normal);

        vec3 surfaceColor =
            td.material.additiveColor
            + lightContribution(eyePos, eyeDir, normal, light1Pos, light1Color, td.material, occlusionTerm)
            + lightContribution(eyePos, eyeDir, normal, light2Pos, light2Color, td.material, occlusionTerm)
            + forest(envTex, eyePos, reflectionVector) *
                mix(td.material.diffuseColor, vec3(1.0), fresnelApproximation) *
                td.material.fresnelAmount;

        color =
            /*mix(
                color,
                surfaceColor,
                1.0 - pow(disTravelled / maxDisTravelled, 2.0))*/
            surfaceColor
            ;
    }

    float exposure = 1.4;
    vec3 toneMappedColor =
        //mix(color, tonemap(color, exposure), step(0.0, pixelPos.x))
        tonemap(color, exposure)
        ;

    vec3 finalColor =
        // "Grading"
        (pow(saturate(toneMappedColor), vec3(1.6)) * .86 + .14)

        // Vignette
        * (1.0 - pow(saturate(length(fragCoord - iResolution.xy / 2.0) / (iResolution.y * 1.58)), 3.2) * 3.6)

        // Purple corner burst
        + (1.0 - saturate(length(fragCoord - vec2(iResolution.x * .22, iResolution.y))
                          / (iResolution.y * 1.03))) * vec3(.4, .1, 1.0) * .4

        // Red corner burst
        + (1.0 - saturate(length(fragCoord - vec2(iResolution.x * 1.1, iResolution.y * .2))
                          / (iResolution.y * .6))) * vec3(1.0, .02, .1) * .2
        ;

    // Scroller
    const float idealWidth = 1920.0;
    const float idealHeight = 1080.0;
    const float originalScrollWidth = 12150.0;
    const float originalScrollHeight = 1080.0;
    const float songLen = 105.0;
    float scrollWidth = originalScrollWidth / ((iResolution.x / iResolution.y) / (idealWidth / idealHeight));
    float scrollOffset = -idealWidth * 1.5 + (scrollWidth + idealWidth * 1.5) * (iGlobalTime / songLen);
    vec2 scrollUv = vec2((fragCoord.x / iResolution.x * idealWidth + scrollOffset) / scrollWidth, 1.0 - fragCoord.y / iResolution.y);
    vec3 scrollColor = texture(scrollTex, saturate(scrollUv)).rgb;
    finalColor += scrollColor;

    finalColor = saturate(finalColor);

    float fade = 1.0;
    if (iGlobalTime < 7.0)
    {
        fade = iGlobalTime / 7.0;
    }
    else if (iGlobalTime >= songLen - 7.0) {
        fade = 1.0 - (iGlobalTime - (songLen - 7.0)) / 7.0;
    }
    finalColor *= fade;

    fragColor = vec4(finalColor, 1.0);
}

// ---- Shadertoy shader ends here ----------------------------------------

out vec4 FragColor;

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