/* 
Day 14: Postponed: bugs.
Day 15: Postponed: out all day, knackered.
Day 16: Postponed: bugs again. Not going well this, is it?
Day 17: Bugs fixed, just needs a few more minutes… crap, time to catch a flight!
Day 18: Such a long flight. Let’s make up for the last few days :)
*/

//#define DEBUG
#ifdef DEBUG
int debugStep = 0;
float debugValue = 0.0;
#endif

// Rotation
#define R(p,a) p=cos(a)*p+sin(a)*vec2(-p.y,p.x);


#define kINFINITY 10000.0 // An unimaginably large number
#define kSQRT2 1.414213
#define kISQRT2 0.707107
#define kPI 3.141592

// maximum iteration count
#define kMAXITERS 120
#define kEPSILON 0.001
#define kMAXINTERSECTIONS 1

// refractive index
#define kREFRACT 1.0/1.5

// materials
#define kFLOORMATERIAL 0
#define kGLASSMATERIAL 1
#define kMIRRORMATERIAL 2

#define kFLOORCOLOUR vec4(0.7, 0.65, 0.6, 1.0)
#define kGLASSCOLOUR vec4(1.0, 0.5, 0.1, 1.0)
#define kMIRRORCOLOUR vec4(1.0, 0.3, 0.3, 1.0)
    
// A ray. Has origin + direction.
struct Ray {
    vec3 origin;
    vec3 dir;
};
    
// Distance to nearest surface
struct SDResult {
    float d; // Distance
    int material; // Nearest material
};

// A camera. Has a position and a direction. 
struct Camera {
    vec3 pos;
    Ray ray;
};
    
// A disk. Has position, size, colour.
struct Disk {
    vec3 pos;
    float radius;
    vec3 col;
};
    
struct Sphere {
    vec3 pos;
    float radius;
};
    
struct Box {
	vec3 pos;
	vec3 size;
    float radius;
};
    
float eps = kEPSILON;
float divergence;

// Normalised random number, borrowed from Hornet's noise distributions: https://www.shadertoy.com/view/4ssXRX
float nrand(in vec2 n) {
	return fract(sin(dot(n.xy, vec2(12.9898, 78.233)))* 43758.5453);
}

vec3 texrand(in vec2 n) {
    return texture(iChannel2, n).xyz;
}

vec3 smoothBlend(in vec3 point, in vec3 about, in float radius) {
    point -= about;
    point = mix(-point, point, smoothstep(-radius, radius, point));
    return point + about;
}
    
// Distance to sphere (signed)
float sphereDist(in Ray ray, in Sphere sphere) {
    return length(ray.origin - sphere.pos) - sphere.radius;
}

// Distance to sphere surface
float uSphereDist(in Ray ray, in Sphere sphere) {
    return abs(length(ray.origin - sphere.pos) - sphere.radius);
}

// Distance to box surface (signed)
float boxDist(in Ray ray, in Box box) {
    vec3 dist = abs(ray.origin - box.pos) - (box.size * 0.5);
    vec3 cDist = max(dist, 0.0);
    return min(max(dist.x, max(dist.y, dist.z)), 0.0) + length(cDist) - box.radius;
}

// Distance to box surface
float uBoxDist(in Ray ray, in Box box) {
    return abs(length(max(abs(ray.origin - box.pos) - (box.size * 0.5), 0.0)) - box.radius);
}

// distance to floor
float floorDist(in Ray ray) {
    float dist = ray.origin.y;
    return dist;
}

float hashForCell(in vec3 pos, in float cellSize) {
    float hash = nrand(floor(pos.xz / cellSize) + 68.0);
    return hash;
}

vec3 randomColourForCell(in vec3 pos, in float cellSize) {
	float hash = hashForCell(pos, cellSize); 
    return vec3(
        nrand(vec2(hash * 2.0, hash * 4.0)),
        nrand(vec2(hash * 4.0, hash * 8.0)),
        nrand(vec2(hash * 8.0, hash * 16.0))
	);
	vec3 c = vec3(hash, mod(hash + 0.15, 1.0), mod(hash + 0.3, 1.0)) * 0.75;
}

/*
---- INTERSECTION OPS ----
*/

// Union of two signed distances
float unionOp(float d0, float d1) {
    return min(d0, d1);
}

// Union of two unsigned distances
float unionOpU(float d0, float d1) {
    return min(d0, d1);
}

// Intersection of two signed distances
float intersectOp(float d0, float d1) {
    return max(d0, d1);
}

// Intersection of two unsigned distances
float intersectOpU(float d0, float d1) {
    return max(d0, d1);
}

// Difference of two signed distances
float differenceOp(float d0, float d1) {
    return max(d0, -d1);
}

// Difference of two unsigned distances
float differenceOpU(float d0, float d1) {
    return max(d0, -d1);
}

// Get the distance to the scene (returns a struct containing distance and nearest material)
SDResult sceneDist(in Ray ray) {
    SDResult result;
    
    // Mess with the ray
    vec2 offset = vec2(
        sin(iTime * 0.34), 
        cos(iTime * 0.55)
    );
    float r = 2.0;
    
    ray.origin.xz += offset;
    R(ray.origin.xz, iTime * 0.34);
    ray.origin = smoothBlend(ray.origin, offset.xyx, r);
    ray.origin.zy += offset;
    R(ray.origin.zy, iTime * 0.55);
    ray.origin = smoothBlend(ray.origin, offset.yxy, r);
    ray.origin.yx += offset;
    R(ray.origin.yx, iTime * 0.55);
    ray.origin = smoothBlend(ray.origin, offset.yyx, r);

    ray.origin.xz -= offset;
    
    Box box = Box(vec3(0), vec3(5), 0.4);
    Box innerBox = Box(vec3(0), vec3(4), 0.4);
    
    // get the distance 
    float distToBox = differenceOpU(
        boxDist(ray, box),
        boxDist(ray, innerBox)
        );
    
    Sphere ball = Sphere(vec3(0), 2.0);
    
    float distToBall = sphereDist(ray, ball);
    
    //Find the neares of the floor and box0
    result.d = unionOp(distToBox, distToBall);
    //result.d = distToHouse;
    result.material = result.d == distToBox ? kGLASSMATERIAL : kMIRRORMATERIAL;
    
    return result;
}

// Gets the normal
vec3 normal(in Ray ray) {
    vec2 eps = vec2(0.0001, 0);
    float baseDist = sceneDist(ray).d;
 	return normalize(vec3(
        sceneDist(Ray(ray.origin + eps.xyy, ray.dir)).d - baseDist,
        sceneDist(Ray(ray.origin + eps.yxy, ray.dir)).d - baseDist,
        sceneDist(Ray(ray.origin + eps.yyx, ray.dir)).d - baseDist
        ));
}

// Moves the ray to the surface. Helps avoid artefacts due to ray intersection imprecision.
void clampToSurface(in Ray ray, in float d, inout vec3 n) {
 	ray.origin += n * d;
 	d = sceneDist(ray).d;
 	n = normal(ray);
}

// Calulcate a fresnel term for reflections
float fresnelTerm(in Ray ray, in vec3 n, in float power) {
	float fresnel = min(1., dot(ray.dir, n) + 1.0);
	fresnel = pow(fresnel, power);
    return fresnel;
}

/*
---- LIGHTING ----
*/

float occlusion(in Ray ray, in vec3 n) {
    float o = 0.0;
    ray.dir = n;
    float x = 0.1;
    for (int i=0; i<5; i++) {
    	ray.origin += x;
        float d = sceneDist(ray).d;
        o += max(x - d, 0.0);
        
        x *= 2.0;
    }
 	return 1.0 - o * 0.5;;
}

// The main marching loop
void marchRay(inout Ray ray, inout vec4 colour) {
    bool inside = false; // are we inside or outside the glass object
    vec4 impact = vec4(1.0); // This decreases each time the ray passes through glass, darkening colours

#ifdef DEBUG   
vec4 debugColour = vec4(1, 0, 0, 1);
#endif
    
    SDResult result;
    vec3 n;
    vec3 glassStartPos;
    
    for (int i=0; i<kMAXITERS; i++) {
        // Get distance to nearest surface
        result = sceneDist(ray);
        
        // Step half that distance along ray (helps reduce artefacts)
        float stepDistance = inside ? abs(result.d) : result.d;
            //result.material == kGLASSMATERIAL ? abs(result.d) : result.d;
        ray.origin += ray.dir * stepDistance * 0.5;
        if (length(ray.origin) > 30.0) { break; }
        
        if (stepDistance < eps) {
            // colision
            // normal
            // Get the normal, then clamp the intersection to the surface
    		n = normal(ray);
            clampToSurface(ray, stepDistance, n);
#ifdef DEBUG
//debugColour = vec4(-n*1.0, 1);
//debugStep++;
//if (debugStep == 3) break;
          //  break;
//if (mod(ray.origin.y, 1.0) > 0.5) break;
//debugValue += 0.25;
#endif
            
            if ( result.material == kFLOORMATERIAL ) {
                // ray hit floor
                
                // Add some noise to the normal, since this is pretending to be grit...
                vec3 randomNoise = texrand(ray.origin.xz * 0.4);
                n = mix(n, normalize(vec3(randomNoise.x, 1, randomNoise.y)), randomNoise.z);
                
                // Colour is just grey with crappy fake lighting...
                colour += mix(
                    kFLOORCOLOUR, 
                    vec4(0,0,0,1), 
                    pow(max((-n.x+n.y) * 0.5, 0.0), 2.0)
                ) * impact;
                impact *= 0.;
                break;
            }
            
            // check what material it is...
            
            if (result.material == kMIRRORMATERIAL) {
                // it's a mirror, reflect the ray
                ray.dir = reflect(ray.dir, n);
                    
                // Step 2x epsilon into object along normal to ensure we're beyond the surface
                // (prevents multiple intersections with same surface)
                ray.origin += n * eps * 2.0;
                
                // Mix in the mirror colour
                impact *= kMIRRORCOLOUR;
                
            } else {
                // glass material
            
                if (inside) {
                	// refract glass -> air
                	ray.dir = refract(ray.dir, -n, 1.0/kREFRACT);
                    
                    // Find out how much to tint (how far through the glass did we go?)
                    float glassTravelDist =  clamp(distance(glassStartPos, ray.origin) / 1.0, 0., 1.);
                    
                    // Get a random colour
                	impact *= mix(vec4(1), kGLASSCOLOUR, glassTravelDist);
                    
#ifdef DEBUG
debugValue += glassTravelDist / 2.0;
#endif
      
                
              	} else {
               		// refract air -> glass
                	glassStartPos = ray.origin;
                    
              	  	// Mix the reflection in, according to the fresnel term
                	float fresnel = fresnelTerm(ray, n, 2.0);
    				colour = mix(
                    	colour, 
                    	texture(iChannel1, reflect(ray.dir, n)), 
                    	vec4(fresnel) * impact);
                    impact *= 1.0 - fresnel;
    			
                	// refract the ray
            		ray.dir = refract(ray.dir, n, kREFRACT);
                    
#ifdef DEBUG
//debugValue += 0.5;
#endif
                }
            
            	// Step 2x epsilon into object along normal to ensure we're beyond the surface
                ray.origin += (inside ? n : -n) * eps * 2.0;
                
                // Flip in/out status
                inside = !inside;
            }
        }
        
        // increase epsilon
        eps += divergence * stepDistance;
    }
    
    // So far we've traced the ray and accumulated reflections, now we need to add the background.
    colour += texture(iChannel0, ray.dir) * impact;
    
    
#ifdef DEBUG
//debugColour.rgb = ray.dir;
debugColour.rgb = vec3(float(debugStep)/2.0);
colour = debugColour;
#endif
}

// Sets up a camera at a position, pointing at a target.
// uv = fragment position (-1..1) and fov is >0 (<1 is telephoto, 1 is standard, 2 is fisheye-like)
Camera setupCam(in vec3 pos, in vec3 target, in float fov, in vec2 uv) {
		// cam setup
    // Create camera at pos
	Camera cam;
    cam.pos = pos;
    
    // A ray too
    Ray ray;
    ray.origin = pos;
    
    // FOV is a simple affair...
    uv *= fov;
    
    // Now we determine hte ray direction
	vec3 cw = normalize (target - pos );
	vec3 cp = vec3 (0.0, 1.0, 0.0);
	vec3 cu = normalize ( cross(cw,cp) );
	vec3 cv = normalize ( cross (cu,cw) );
    
	ray.dir = normalize ( uv.x*cu + uv.y*cv + 0.5 *cw);
    
    // Add the ray to the camera and our work here is done.
	cam.ray = ray;
    
    // Ray divergence
    divergence = fov / iResolution.x;
    
	return cam;
}

vec3 camPath(in float time) {
    float r = 10.0;
    return vec3(sin(time) * r, sin(time*2.) * 3.0, cos(time) + r);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // We'll need a camera. And some perspective.
    
	// Get some coords for the camera angle from the frag coords. Convert to -1..1 range.
    vec2 uv = fragCoord.xy / iResolution.xy;
    uv = uv * 2. - 1.;
    
    // Aspect correction so we don't get oval bokeh
    uv.y *= iResolution.y/iResolution.x;
    
    // Make a camera with ALL NEW AND IMPROVED! camera code :)
    float camTime = iTime / 4.0;
    vec3 camPos = camPath(camTime);
    vec3 camTarget = vec3(0);
    //camTarget.y -= 3.0;
    Camera cam = setupCam(camPos, camTarget, 0.500, uv);
    
    // Let's raymarch some stuff and inject that into the scene...
    
    // Create an empty colour
    vec4 col = vec4(0.0);
    
    // Trace that ray!
    marchRay(cam.ray, col);
    
	fragColor = vec4(col.rgb,1.0);
}
