(function() {

var V3 = function(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
}
V3.prototype = {
    // iop -> inplace
    // ops -> scalar
    add: function(v) {
        return new V3(this.x+v.x, this.y+v.y, this.z+v.z);
    },
    iadd: function(v) {
        this.x += v.x;
        this.y += v.y;
        this.z += v.z;
    },
    sub: function(v) {
        return new V3(this.x-v.x, this.y-v.y, this.z-v.z);
    },
    mul: function(v) {
        return new V3(this.x*v.x, this.y*v.y, this.z*v.z);
    },
    div: function(v) {
        return new V3(this.x/v.x, this.y/v.y, this.z/v.z);
    },
    muls: function(s) {
        return new V3(this.x*s, this.y*s, this.z*s);
    },
    divs: function(s) {
        return this.muls(1.0/s);
    },
    dot: function(v) {
        return this.x*v.x+this.y*v.y+this.z*v.z;
    },
    normalize: function(){
        return this.divs(Math.sqrt(this.dot(this)));
    }
};

/*
 * This is my crude way of generating random normals in a hemisphere.
 * In the first step I generate random vectors with components
 * from -1 to 1. As this introduces a bias I discard all the points
 * outside of the unit sphere. Now I've got a random normal vector.
 * The last step is to mirror the point if it is in the wrong hemisphere.
 */
function getRandomNormalInHemisphere(v){
    do {
        var v2 = new V3(Math.random()*2.0-1.0, Math.random()*2.0-1.0, Math.random()*2.0-1.0);
    } while(v2.dot(v2) > 1.0);
    // should only require about 1.9 iterations of average
    v2 = v2.normalize();
    // if the point is in the wrong hemisphere, mirror it
    if(v2.dot(v) < 0.0) {
        return v2.muls(-1);
    }
    return v2;
}

/*
 * The camera is defined by an eyepoint (origin) and three corners
 * of the view plane (it's a rect in my case...)
 */
var Camera = function(origin, topleft, topright, bottomleft) {
    this.origin = origin;
    this.topleft = topleft;
    this.topright = topleft;
    this.bottomleft = bottomleft;

    this.xd = topright.sub(topleft);
    this.yd = bottomleft.sub(topleft);
}
Camera.prototype = {
    getRay: function(x, y) {
        // point on screen plane
        var p = this.topleft.add(this.xd.muls(x)).add(this.yd.muls(y));
        return {
            origin: this.origin,
            direction: p.sub(this.origin).normalize()
        };
    }
};

var Sphere = function(center, radius) {
    this.center = center;
    this.radius = radius;
    this.radius2 = radius*radius;
};
Sphere.prototype = {
    // returns distance when ray intersects with sphere surface
    intersect: function(ray) {
        var distance = ray.origin.sub(this.center);
        var b = distance.dot(ray.direction);
        var c = distance.dot(distance) - this.radius2;
        var d = b*b - c;
        return d > 0 ? -b - Math.sqrt(d) : -1;
    },
    getNormal: function(point) {
        return point.sub(this.center).normalize();
    }
};

var Material = function(color) {
    this.color = color;
}

var Body = function(shape, material) {
    this.shape = shape;
    this.material = material;
}

var Renderer = function(scene) {
    this.scene = scene;
    this.iterations = 0;
    this.buffer = [];
    for(var i = 0; i < scene.output.width*scene.output.height;i++){
        this.buffer.push(new V3(0.0, 0.0, 0.0));
    }

}
Renderer.prototype = {
    clearBuffer: function() {
        for(var i = 0; i < this.buffer.length; i++) {
            this.buffer[i].x = 0.0;
            this.buffer[i].y = 0.0;
            this.buffer[i].z = 0.0;
        }
    },
    iterate: function() {
        var scene = this.scene;
        var w = scene.output.width;
        var h = scene.output.height;
        var i = 0;
        // the ternary operator is a hack to do 2x antialiasing
        // for every odd iterations each pixel is shifted half
        // a pixel to the left.
        for(var y = this.iterations & 1 ? 0 : 0.5/h, ystep = 1.0/h; y < 1.0; y += ystep){
            for(var x = this.iterations & 1 ? 0 : 0.5/w, xstep = 1.0/w; x < 1.0; x += xstep){
                var ray = scene.camera.getRay(x, y);
                var color = this.trace(ray, 0);
                this.buffer[i++].iadd(color);
            }
        }
        this.iterations += 1;
    },
    trace: function(ray, n) {
       var mint = Infinity;
        var hit = null;
        for(var i = 0; i < this.scene.objects.length;i++){
            var o = this.scene.objects[i];
            var t = o.shape.intersect(ray);
            if(t > 0 && t <= mint) {
                mint = t;
                hit = o;
            }
        }

        // the sky is the only source of light right now
        if(hit == null) {
            return this.scene.sky.color;
        }
        // russian roulett, I hope the n > 3 doesn't introduce bias
        if(n > 3 && Math.random() > 0.5) {
            return new V3(0.0, 0.0, 0.0);
        }
 
        // perfect diffuse reflection
        var point = ray.origin.add(ray.direction.muls(mint));
        var normal = hit.shape.getNormal(point);
        var direction = getRandomNormalInHemisphere(normal);
        var newray = {origin: point, direction: direction};
        return this.trace(newray, n+1).mul(hit.material.color);
    }
}

var main = function(width, height, iterationsPerMessage, serialize) {
    var scene = {
        output: {width: width, height: height},
        sky: new Material(new V3(0.8, 1.0, 1.2)),
        camera: new Camera(
            new V3(0.0, -0.5, 0.0),
            new V3(-1.0, 1.0, 1.0),
            new V3(1.0, 1.0, 1.0),
            new V3(-1.0, 1.0, -1.0)
        ),
        objects: [
            new Body(new Sphere(new V3(0.0, 3.5, 0.0), 0.5), new Material(new V3(0.9, 0.9, 0.9))),
            new Body(new Sphere(new V3(1.2, 3.5, -0.1), 0.5), new Material(new V3(0.95, 0.2, 0.1))),
            new Body(new Sphere(new V3(-1.2, 3.5, -0.1), 0.5), new Material(new V3(0.3, 0.1, 0.9))),
            new Body(new Sphere(new V3(0.0, 3.5, -10), 9.5), new Material(new V3(0.9, 0.9, 0.9)))
        ]
    }
    var renderer = new Renderer(scene);
    var buffer = [];
    while(true) {
        for(var x = 0; x < iterationsPerMessage; x++) {
            renderer.iterate();
        }
        postMessage(serializeBuffer(renderer.buffer, serialize));
        renderer.clearBuffer();
    }
}

var serializeBuffer = function(rbuffer, json) {
    var buffer = [];
    for(var i = 0; i < rbuffer.length; i++){
        buffer.push(rbuffer[i].x);
        buffer.push(rbuffer[i].y);
        buffer.push(rbuffer[i].z);
    }
    return json ? JSON.stringify(buffer) : buffer;
}

onmessage = function(message) {
    var data = message.data;
    var serialize = false;
    // the current stable versions of chrome
    // only pass strings as messages in that
    // case I use native json for serializing
    // the data
    if(typeof(data) == 'string') {
        data = JSON.parse('['+data+']');
        serialize = true;
    }
    main(data[0], data[1], data[2], serialize);
}

})();

