/*
 * javascript voxel demo 
 *
 * Copyright (c) 2009 Selim Arsever (voxel.onaluf.org)
 * licensed under the MIT (MIT-LICENSE.txt)
 */
 
 
var constants = {
    highres: false,
    screen: {
        height:    100,
        width:     160,
        zoom:      4
    },
    
    pov: {
        verticalOpening:   0.4,
        depthOfField:     900
    },
    
    color: {
        fog: [216, 247, 255]
    },
    
    init: function() {
        this.pov.horizontalOpening = Math.atan(constants.pov.verticalOpening) * constants.screen.width / constants.screen.height;
        this.screen.distance       = constants.screen.width / 2 / Math.tan(constants.pov.horizontalOpening);
    }
};
 
var voxel = (function(){

    return {
        position: {
            x: 450, 
            y: 0, 
            z: 180,
            a: Math.PI/2,
            antialiasing: false
        },
        frameCounter: 0,
        
        loadHeightmap: function(heighmapFilename, textureFilename) {
            var img    = new Image();
            img.onload = function() {
                    // convert the image to an ImageData
                    var imageData = canvasTools.imageToImageData(img);
                    // convert the imageData to an array
                    voxel.heightmap = canvasTools.imageDataToArrayR(imageData);
                    // Load the texture
                    voxel.loadTexture(textureFilename);
                };
            img.src = heighmapFilename;
        },
        
        loadTexture: function(textureFilename) {
            var img    = new Image();
            img.onload = function() {
                    // draw the image to to the canvas
                    var imageData = canvasTools.imageToImageData(img);
                    // convert the imageData to an array
                    voxel.texture = canvasTools.imageDataToArray(imageData);
                    // now we can continue 
                    voxel.imagesLoaded();
                };
            img.src = textureFilename;
        },
        
        imagesLoaded: function() {
            // initialize constants:
            constants.init();
            
            // create a sort of double buffer (two context):
            // first a offscreen context
            this.offscreenCanvas        = document.createElement('canvas');;
            this.offscreenCanvas.width  = constants.screen.width;
            this.offscreenCanvas.height = constants.screen.height;
            this.offscreenContext       = this.offscreenCanvas.getContext("2d");        
            
            // second the onscreen context
            this.onscreenCanvas         = document.getElementById("canvas");
            this.onscreenCanvas.width   = constants.screen.width  * constants.screen.zoom;
            this.onscreenCanvas.height  = constants.screen.height * constants.screen.zoom;
            this.onscreenContext        = this.onscreenCanvas.getContext("2d");
            //this.onscreenContext.scale(constants.screen.zoom, constants.screen.zoom);
            
            // get an ImageData to draw each frame
            this.frame = voxel.offscreenContext.getImageData(0, 0, constants.screen.width, constants.screen.height);
            
            // send all important data to the worker:
            this.startTime = new Date().getTime();
            this.renderFrame();
        },
        
        renderFrame: function() {
            // Clean the frame to the sky color
            voxel.offscreenContext.fillStyle = "rgb("+constants.color.fog[0]+","+constants.color.fog[1]+","+constants.color.fog[2]+")";
            voxel.offscreenContext.fillRect(0, 0, constants.screen.width, constants.screen.height);
            voxel.frame = voxel.offscreenContext.getImageData(0, 0, constants.screen.width, constants.screen.height);
            
            for (var i = 0; i < constants.screen.width; i++){
                var orientation        = voxel.position.a - constants.pov.horizontalOpening*(1-i*2/constants.screen.width);
                var progression        = {x: Math.cos(orientation), y: Math.sin(orientation)};
                var distanceProbed     = 0;
                var screenProjectedTop = 0;
                var doff               = constants.pov.depthOfField / 4;
                var c1                 = constants.screen.height /2;
                var c2                 = constants.screen.distance * voxel.position.z;
                
                var oldHeight          = 0;
                
                while(distanceProbed < constants.pov.depthOfField && screenProjectedTop < constants.screen.height) {
                    // 1) find the projection of the current point on the screen space
                    distanceProbed  +=  (distanceProbed < doff)? 2 : (distanceProbed < 2 * doff)? 4 : (distanceProbed < 3 * doff)? 8 : 16;
                    var probe = { //warp for texture
                            x: Math.abs(Math.ceil(voxel.position.x + distanceProbed * progression.x)),
                            y: Math.abs(Math.ceil(voxel.position.y + distanceProbed * progression.y))
                        };
                    var dataIndex        = voxel.heightmap.width * (probe.y % voxel.heightmap.height) + (probe.x % voxel.heightmap.width);
                    
                    // This is a small optimisation to skip some projection computation
                    var height           = voxel.heightmap.data[dataIndex];  
                    if(height < oldHeight){
                        oldHeight = height;
                        continue;
                    }
                    oldHeight = height;
                    
                    var projectedHeight  = Math.min(Math.ceil(c1 - (c2 - constants.screen.distance*height) / distanceProbed), constants.screen.height);
                    
                    // 2) if visible we draw it
                    if (projectedHeight > screenProjectedTop) {
                        var textureDataIndex = (voxel.texture.width * (probe.y % voxel.texture.height) + (probe.x % voxel.texture.width)) * 4;
                        var textureCache     = [voxel.texture.data[textureDataIndex], voxel.texture.data[textureDataIndex + 1], voxel.texture.data[textureDataIndex + 2]];
                        
                        var fillGoal     = Math.max(constants.screen.height-projectedHeight, 0);
                        var fogFactor    = Math.min(distanceProbed, constants.pov.depthOfField)/constants.pov.depthOfField;
                        var invFogFactor = (1 - fogFactor);
                        
                        // antialiasing
                        var renderCache = {
                                r: invFogFactor * textureCache[0] + fogFactor * constants.color.fog[0], 
                                g: invFogFactor * textureCache[1] + fogFactor * constants.color.fog[1], 
                                b: invFogFactor * textureCache[2] + fogFactor * constants.color.fog[2]
                            }
                        if(voxel.position.antialiasing && oldRenderCache && summit) {
                            var previousIndex = (i + constants.screen.width * (constants.screen.height-screenProjectedTop+1))*4;
                            voxel.frame.data[previousIndex]   = Math.ceil(0.5*renderCache.r + 0.5*oldRenderCache.r);
                            voxel.frame.data[previousIndex+1] = Math.ceil(0.5*renderCache.g + 0.5*oldRenderCache.g);
                            voxel.frame.data[previousIndex+2] = Math.ceil(0.5*renderCache.b + 0.5*oldRenderCache.b);
                        }
                        var oldRenderCache = renderCache;
                        
                        // render
                        for (var j = (constants.screen.height - screenProjectedTop); j > fillGoal; j--) {
                            voxel.frame.data[(i+j*constants.screen.width)*4]   = Math.ceil(renderCache.r);
                            voxel.frame.data[(i+j*constants.screen.width)*4+1] = Math.ceil(renderCache.g);
                            voxel.frame.data[(i+j*constants.screen.width)*4+2] = Math.ceil(renderCache.b);
                            voxel.frame.data[(i+j*constants.screen.width)*4+3] = 255;
                        }
                        screenProjectedTop = projectedHeight;
                        summit = false;
                    } else if (screenProjectedTop > projectedHeight){
                        var summit = true;
                    }
                }
                
                // 3) if the top is lower than the top of the screen we fill it
                if(voxel.position.antialiasing){
                    if((constants.screen.height - screenProjectedTop + 1) >= 0) {
                        var j = (i + (constants.screen.height - screenProjectedTop + 1 ) * constants.screen.width)*4;
                        voxel.frame.data[j]   = Math.ceil(0.5*voxel.frame.data[j]   + 0.5*constants.color.fog[0]);
                        voxel.frame.data[j+1] = Math.ceil(0.5*voxel.frame.data[j+1] + 0.5*constants.color.fog[1]);
                        voxel.frame.data[j+2] = Math.ceil(0.5*voxel.frame.data[j+2] + 0.5*constants.color.fog[2]);
                    }
                }
            }
            voxel.offscreenContext.putImageData(voxel.frame, 0, 0);
            voxel.onscreenContext.drawImage(voxel.offscreenCanvas, 0, 0, constants.screen.zoom*constants.screen.width, constants.screen.zoom*constants.screen.height);
            //onscreenContext.drawImage(offscreenCanvas, 0, 0);
            
            // mesure framerate
            voxel.frameCounter++;
            
            document.getElementById("fps").innerHTML = (Math.floor(voxel.frameCounter/(new Date().getTime()-voxel.startTime)*10000)/10)+"fps";
            if(voxel.frameCounter > 30) {
                voxel.frameCounter = 0;
                voxel.startTime = new Date().getTime();
            }
            
            setTimeout(arguments.callee, 1);
        },
        
        eventHandler: function(e) {
            var increment = 10;
            switch(e.keyCode) {
                case 37: //left
                    this.position.a -= 0.03;
                    break;
                case 38: //up 
                    this.position.x += Math.cos(this.position.a)*increment;
                    this.position.y += Math.sin(this.position.a)*increment;
                    break;
                case 39: //right
                    this.position.a += 0.03;
                    break;
                case 40: //down
                    this.position.x -= Math.cos(this.position.a)*increment;
                    this.position.y -= Math.sin(this.position.a)*increment;
                    break;
                case 87: // w = UP
                    this.position.z += 10;
                    break;
                case 83: // s = DOWN
                    this.position.z -= 10;
                    break;
                case 65: // a : antialiasing
                    this.position.antialiasing = !this.position.antialiasing;
                    break;
                case 81: //q : swich resolution
                    constants.highres = !constants.highres;
                    if(constants.highres){
                        constants.screen.height = 200;
                        constants.screen.width  = 320;
                        constants.screen.zoom   = 2;
                    } else {
                        constants.screen.height = 100;
                        constants.screen.width  = 160;
                        constants.screen.zoom   = 4;
                    }
                    // initialize constants:
                    constants.init();
                    
                    // create a sort of double buffer (two context):
                    // first a offscreen context
                    this.offscreenCanvas        = document.createElement('canvas');;
                    this.offscreenCanvas.width  = constants.screen.width;
                    this.offscreenCanvas.height = constants.screen.height;
                    this.offscreenContext       = this.offscreenCanvas.getContext("2d");        
                    
                    // second the onscreen context
                    this.onscreenCanvas         = document.getElementById("canvas");
                    this.onscreenCanvas.width   = constants.screen.width  * constants.screen.zoom;
                    this.onscreenCanvas.height  = constants.screen.height * constants.screen.zoom;
                    this.onscreenContext        = this.onscreenCanvas.getContext("2d");
                    
                    // get an ImageData to draw each frame
                    this.frame = voxel.offscreenContext.getImageData(0, 0, constants.screen.width, constants.screen.height);
            }
            this.keyTrap(e);
            return false; 
        },
        
        mouseHandler: function(event) {
            if(event.type == "mousedown"){
                if(event.button == 0) {
                    this.pressed = true;
                }
            } else if (event.type == "mouseup") {
                if(event.button == 0) {
                    this.pressed = false;
                }
            } else if (event.type == "mousemove") {
                if(this.pressed) {
                    var xdiff = event.clientX - this.oldClientX;
                    this.position.a += 0.01*xdiff;
                    
                    var ydiff = event.clientY - this.oldClientY;
                    this.position.x -= Math.cos(this.position.a)*2*ydiff;
                    this.position.y -= Math.sin(this.position.a)*2*ydiff;
                }
            }
            this.oldClientX = event.clientX;
            this.oldClientY = event.clientY;
        }, 
        
        keyTrap: function (event) {
            var event = event || window.event;
            switch (event.keyCode) {
                case 37:
                case 38:
                case 39:
                case 40:
                    if (event.preventDefault) {
                        event.preventDefault();
                    } else {
                        event.returnValue = false;
                    }
                    break;
            }
        },
        
        start: function(heighmapFilename, textureFilename) {
            this.loadHeightmap(heighmapFilename, textureFilename);
        }
    };
})();

var canvasTools = {
    imageToImageData: function (image){
        // draw the image to to the canvas
        var canvas        = document.createElement('canvas');
        canvas.width      = image.width;
        canvas.height     = image.height;
        
        var context = canvas.getContext("2d");
        context.drawImage(image, 0, 0);
        
        // return the imageData
        return context.getImageData(0, 0, image.width, image.height);
    },
    
    imageDataToArrayR: function (imageData) {
        // create an exportable array instead of the data part
        // but with only the red component
        var temp = {
            width:  imageData.width,
            height: imageData.height,
            data:   []
        }
        for(var i = 0; i < imageData.data.length / 4; i++) {
            temp.data[i] = imageData.data[i*4];
        }
        return temp;
    },
    
    imageDataToArray: function (imageData) {
        // create an exportable array instead of the data part
        var temp = {
            width:  imageData.width,
            height: imageData.height,
            data:   []
        }
        for(var i = 0; i < imageData.data.length; i++) {
            temp.data[i] = imageData.data[i];
        }
        return temp;
    }
};