/**
 * EarthGL.Map setups a canvas tag where Layer and Section can be added
 * @param {String} id Identifier of the HTML div tag
 * @param {Object} options Optional properties for configuring the map
 * @constructor
 */

EarthGL.Map = EarthGL.Class({
	// perspective parameters
	// field of view
	fovy: 30,
	zNear: 1,
	zFar: 1000,
	mindist: 1.25,
	stepdist: 0.25,

	initialize: function(id,options) {
	    this.id = id;
	    this.topo = null;
	    this.nLon = 64;
	    this.nLat = 32;
	    this.EarthRadius = 6400e3; // in m
	    this.vscale = 1;
	    this.distance = 6; // in EarthRadius
	    this.baseLayer = null;
	    this.controls = [];
	    this.rotating = false;
	    this.clearDepth = 10000;
	    this.clearColor = [ 0, 0, 0, 1 ];
	    this.latC = 0;
	    this.lonC = 0;

	    // all layers and sections
	    this.layers = [];
	    this.sections = [];

	    // private fields
	    this._objects = [];

	    // overwrite default options
	    if (options) {
		for (var p in options) {
		    this[p] = options[p];
		}
	    }

	    // event handler
	    this.events = new EarthGL.Events(this);

	    if (this.topo) {
		this.topo.scale(this.EarthRadius);	
	    }

	    this.elem = document.getElementById(this.id);

	    // view port div has the same size than this.elem 
	    // but has the CSS position relative to allow to set
	    // elements with position absolute

	    this.viewPortDiv = document.createElement('div');
	    this.viewPortDiv.id = this.id + '_viewPortDiv';
	    this.viewPortDiv.style.position = 'relative';
	    this.viewPortDiv.style.width = "100%";
	    this.viewPortDiv.style.height = "100%";	    
	    this.elem.appendChild(this.viewPortDiv);

	    this.canvas = document.createElement('canvas');
	    this.canvas.width = $(this.elem).width();
	    this.canvas.height = $(this.elem).height();
	    this.canvas.id = this.id + '_canvas';
	    this.canvas.className = 'earthgl-canvas';
	    this.viewPortDiv.appendChild(this.canvas);


	    // control elements
    
	    this.addControl(new EarthGL.Control.Navigation());
	    this.addControl(new EarthGL.Control.ZoomIn());
	    this.addControl(new EarthGL.Control.ZoomOut());
	    //this.addControl(new EarthGL.Control.Config());
	    //this.addControl(new EarthGL.Control.Rotate());
	    //this.addControl(new EarthGL.Control.CameraPosition());


	    this._init_webgl();
       
	    this.draw();
	},

	_init_webgl: function() {
	    this.vshaderTopo = '\n\
        uniform mat4 u_modelViewMatrix;\n\
        uniform mat4 u_modelViewProjMatrix;\n\
        uniform mat4 u_normalMatrix;\n\
        uniform vec3 lightDir;\n\
        uniform float vscale;\n\
\n\
        attribute vec4 vTexCoord;\n\
        attribute vec4 elevation;\n\
        attribute vec2 gradh;\n\
        \n\
        varying float v_Dot;\n\
        varying vec2 v_texCoord;\n\
        varying float r;\n\
	varying vec4 vPosition;\n\
	varying vec3 vNormal;\n\
	varying float sinTheta;\n\
	varying float cosTheta;\n\
	varying float sinPhi;\n\
	varying float cosPhi;\n\
\n\
	/* unit vectors */\n\
	varying vec3 er;\n\
	varying vec3 ephi;\n\
	varying vec3 etheta;\n\
\n\
	varying float theta;\n\
	varying float phi;\n\
\n\
        void main()\n\
        {\n\
\n\
	    /* convert to angles in radian*/ \n\
            phi = (-elevation.x) * 3.1415927/180.;\n\
	    theta = (90.-elevation.y) * 3.1415927/180.;\n\
	    \n\
	    sinTheta = sin(theta);\n\
	    cosTheta = cos(theta);\n\
	    sinPhi = sin(phi);\n\
            cosPhi = cos(phi);\n\
\n\
	    /* vscale is the exageration factor*/	\n\
            r = (1.+ vscale*elevation.z);\n\
\n\
\n\
	    /*er = vec3(cosPhi*sinTheta, cosTheta, sinPhi*sinTheta);*/	\n\
	    er = vec3(sinPhi*sinTheta, cosPhi*sinTheta, cosTheta);	    \n\
\n\
            /* spherical coordinates */			    \n\
	    vPosition = vec4(r * er, elevation.w);	    \n\
	    vNormal = er;\n\
	\n\
	    if (sinTheta != 0.) {\n\
	        ephi   = vec3(cosPhi, -sinPhi, 0.);\n\
	        etheta = vec3(sinPhi*cosTheta, cosPhi*cosTheta, -sinTheta);\n\
\n\
	        /* gradh.s is the derivative along phi\n\
	           gradh.t is the derivative along theta */	\n\
\n\
    	        vNormal += -vscale*gradh.s/(r*sinTheta) * ephi;\n\
    	        vNormal += -vscale*gradh.t/r * etheta;\n\
\n\
	        /* check what this function does */	\n\
	        vNormal = normalize(vNormal);	        \n\
	    } \n\
\n\
            gl_Position = u_modelViewProjMatrix * vPosition;\n\
            v_texCoord = vTexCoord.st;\n\
            vec4 transNormal = u_normalMatrix * vec4(vNormal,1.);\n\
            /*v_Dot = max(dot(transNormal.xyz, lightDir), 0.0); */	\n\
	    v_Dot = abs(dot(transNormal.xyz, lightDir));\n\
        }\n\
';


	    this.vshader = '\n\
        uniform mat4 u_modelViewMatrix;\n\
        uniform mat4 u_modelViewProjMatrix;\n\
        uniform mat4 u_normalMatrix;\n\
        uniform vec3 lightDir;\n\
        uniform float vscale;\n\
\n\
        attribute vec3 vNormal;\n\
        attribute vec4 vTexCoord;\n\
        attribute vec4 vPosition;\n\
        \n\
        varying float v_Dot;\n\
        varying vec2 v_texCoord;\n\
        varying float r;\n\
	varying vec4 vPosition2;\n\
\n\
        void main()\n\
        {\n\
	    r = sqrt(vPosition.x*vPosition.x + vPosition.y*vPosition.y + vPosition.z*vPosition.z) ;\n\
	    /* scale the vertical direction*/				\n\
	    vPosition2 = vec4((1. + vscale*(r-1.))/r * vPosition.xyz,vPosition.w);\n\
\n\
            gl_Position = u_modelViewProjMatrix * vPosition2;\n\
            v_texCoord = vTexCoord.st;\n\
            vec4 transNormal = u_normalMatrix * vec4(vNormal,1);\n\
            /* v_Dot = max(dot(transNormal.xyz, lightDir), 0.0); */	\n\
	    v_Dot = abs(dot(transNormal.xyz, lightDir));\n\
        }\n\
';

	    this.fshader = '\n\
#ifdef GL_ES\n\
    precision mediump float;\n\
#endif\n\
\n\
        uniform sampler2D sampler2d;\n\
        uniform float opacity;\n\
\n\
        varying float v_Dot;\n\
        varying vec2 v_texCoord;\n\
        \n\
        void main()\n\
        {\n\
            vec4 color = texture2D(sampler2d,v_texCoord);\n\
	    /* alpha has to be zero */ \n\
            color += vec4(0.1,0.1,0.1,0.);\n\
	    color.a = opacity * color.a;\n\
	    /* color depends on orientation */			\n\
            gl_FragColor = vec4(color.xyz * v_Dot, color.a);\n\
            /*gl_FragColor = color; */ 			    \n\
        }\n\
';
	    var gl;
	   
	    gl = this.canvas.getContext("webgl");

	    if (!gl) {
		gl = this.canvas.getContext("experimental-webgl");

		if (!gl) {
		    throw('no WebGL');
		}
	    }
    
	    // Add a console
	    gl.console = ("console" in window) ? window.console : { log: function() { } };

	    // create our shaders
	    gl.program = EarthGL.Util.createProgram(gl,this.vshaderTopo,this.fshader,
						    [ "vTexCoord", "elevation",'gradh']);

	    gl.useProgram(gl.program);

	    gl.clearColor(this.clearColor[0], this.clearColor[1], 
			  this.clearColor[2], this.clearColor[3]);
	    gl.clearDepth(this.clearDepth);

	    gl.enable(gl.DEPTH_TEST);
	    gl.enable(gl.BLEND);
	    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

	    // constant values passed to the shaders
	    gl.uniform3f(gl.getUniformLocation(gl.program, "lightDir"), 0, 1, 0);
	    gl.uniform1i(gl.getUniformLocation(gl.program, "sampler2d"), 0);
       

	    gl.enable(gl.TEXTURE_2D);


	    gl.secProgram = EarthGL.Util.createProgram(gl,this.vshader,this.fshader,
						       [ "vNormal", "vTexCoord", "vPosition"]);

	    gl.useProgram(gl.secProgram);
	    gl.uniform3f(gl.getUniformLocation(gl.secProgram, "lightDir"), 0, 1, 0);
	    gl.uniform1i(gl.getUniformLocation(gl.secProgram, "sampler2d"), 0);


	    this.gl = gl;
	},

	_rotate_step: function() {
	    if (!this.rotating) {
		return;
	    }

	    this.lonC += 20;
	    this.draw();
	    var that = this;
	    setInterval(function() { that._rotate_step() },1000);
	},

	rotate: function() {
	    this.rotating = !this.rotating;
	    this._rotate_step();
	},

	_ModelView: function() {

	    this.mvMatrix = new J3DIMatrix4();
	    this.mvMatrix.rotate(this.latC, -1, 0, 0);
	    this.mvMatrix.rotate(this.lonC, 0, 0, -1);

	    // construct the normal matrix from the model-view matrix
	    this.normalMatrix = new J3DIMatrix4(this.mvMatrix);
	    this.normalMatrix.invert();
	    this.normalMatrix.transpose();

	    // construct the model-view * projection matrix
	    this.mvpMatrix = new J3DIMatrix4(this.perspectiveMatrix);
	    this.mvpMatrix.multiply(this.mvMatrix);           
	},

	// computes the radius of disk of the earth visible from the 
	// current camera's position

	getVisisbleDiskRadius: function() {
	    var beta;
	    // alpha: max field of view
	    // need to be optimized???
	    var alpha = this.fovy*Math.PI/180;
	    var d = this.distance;

	    var t = d*Math.sin(alpha);
	    var determ = 1 - t*t;

	    // check if full disk is visible
	    if (determ > 0) {
		// disk is only partially visible

		lambda = d*Math.cos(alpha) - Math.sqrt(determ);

		var x = [];
		x[0] = lambda*Math.cos(alpha) - d;
		x[1] = lambda*Math.sin(alpha);
  
		// should be 1: x[0]*x[0] + x[1]*x[1]

		beta = Math.PI - Math.atan2(x[1],x[0]);
	    }
	    else {
		// disk is completely visible

		beta = Math.acos(1/d);
	    }

	    // convert to degrees
	    beta = beta*180/Math.PI;

	    return beta;
	},

	// computes the bounding box of the earth visible at the 
	// current camera's position
	// if factor is ommited, it gets the smallest bbox
	// if factor is greater than one, it gets reasonable 
	//   larger bbox



	getBBOX: function(factor) {
	    factor = factor || 1;
	    var beta = this.getVisisbleDiskRadius();

	    if (beta > 70 || factor > 1) {
		// don't bother a get the full globle

		return {lon: [-180,180],
			lat: [-90,90]};
 
	    }
	    
	    beta = factor * beta;

	    var lon = (this.lonC+180) % 360 - 180;
	    return {lon: [lon-beta, lon+beta],
		    lat: [Math.max(-90,this.latC-beta), 
			  Math.min(90,this.latC+beta)]};
	},

	draw: function() {
	    //console.log('draw',this.lonC);
	    var gl = this.gl;

	    this.reshape();
	    //var angle = atan(1/this.distance);

	    // compute model view matrix
	    this._ModelView();
	    
	    // compute visible portion
	    this.bbox = this.getBBOX();
	    console.log('bbox',this.bbox);

	    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

	    // base layer
	    if (this.baseLayer) {
		this.baseLayer._draw(gl.program);
	    }

	    var i;

	    // vertical sections
	    for (i=0; i < this.sections.length; i++) {
		this.sections[i]._draw(gl.secProgram);
	    }    

	    // horizontal sections
	    for (i=0; i < this.layers.length; i++) {
		this.layers[i]._draw(gl.program);
	    } 
    
	    gl.flush();
	},


	redraw: function() {
	    this.draw();
	},

	reshape: function() {
	    var gl = this.gl;

	    var canvas = this.canvas;
	    /*
	      if (canvas.clientWidth == width && canvas.clientHeight == height)
	      return;
	    */
	    var width = canvas.clientWidth;
	    var height = canvas.clientHeight;
            
	    // Set the viewport and projection matrix for the scene
	    gl.viewport(0, 0, width, height);

	    // Earth is at 0,0,0
	    // eye is at 0,0,this.distance
	    //console.log('this.distance ' + this.distance,this.latC,this.lonC);

	    this.perspectiveMatrix = new J3DIMatrix4();
	    this.perspectiveMatrix.perspective(this.fovy, width/height, this.zNear, this.zFar);
	    this.perspectiveMatrix.lookat(0, this.distance, 0, 0, 0, 0, 0, 0, 1);

	},

	/**
	 * Set the Topography of the the earth with will be used as base-layer
	 * @param {EarthGL.Topo} topo object representing the topography
	 */


	setTopo: function(topo) {
	    var that = this;
	    this.topo = topo;
	    this.topo.scale(this.EarthRadius);
	    var lon = [-180, 180];
	    var lat = [-90, 90];
	    // lat = [0, 90];
	    //var lon;
	    //var lat;
	    var p = {bbox: lon[0] + ',' + lat[0] + ',' + lon[1] + ',' + lat[1]};

	    this.baseLayer.model = EarthGL.Util.makeEarthTex(this.gl, this.baseLayer.getFullRequestString(p),
							     this.topo,
							     function() {
								 // update scene
								 that.draw();
							     },
							     lon,lat
							     );

	},


	setVScale: function(vscale) {
	    this.vscale = vscale;
	    this.draw();
	},

	setDistance: function(dist) {

	    if (dist < this.mindist) {
		dist = this.mindist;
	    }

	    this.distance = dist;
	    this.draw();
	},

	getDistance: function() {
	    return this.distance;
	},

	setCenter: function(lon,lat) {
	    this.latC = lat;
	    this.lonC = lon;
	    this.events.triggerEvent('moveend');
	    this.draw();
	},

	_addObject: function(obj) {
	    // add obj, in _objects farer away objects are first
	    // this._objects[i].distance decreases with increasing i
    
	    for (var i=0; i < this._objects.length; i++) {
		if (obj.distance > this._objects[i].distance) {
		    break;
		}
	    }

	    this._objects.splice(i,0,obj);

	},


	addLayer: function(layer) {
	    layer.setMap(this);

	    if (layer instanceof EarthGL.Layer) {
		if (layer.isBaseLayer) {
		    this.baseLayer = layer;
		}
		else {
		    this.layers.push(layer);
		}
	    }
	    else {
		this.sections.push(layer);
	    }
    
	},

	// remove layer from map

	removeLayer: function(layer) {
	    if (layer instanceof EarthGL.Layer) {
		for (var i=0; i < this.layers.length; i++) {
		    if (this.layers[i] == layer) {
			//console.log('Layer found ', layer.name, layer.title);

			this.layers.splice(i,1);
			this.redraw();

			return;
		    }
		}
	    }
	    else {
		for (var i=0; i < this.sections.length; i++) {
		    if (this.sections[i] == layer) {
			this.sections.splice(i,1);
			this.redraw();

			return;
		    }
		}
	    }

	    console.log('Layer not found ', layer.name, layer.title);
	},

	addSection: function(section) {
	    section.setMap(this);

	    this.sections.push(section);
	},


	// remove section from map

	removeSection: function(section) {
	    for (var i=0; i < this.sections.length; i++) {
		if (this.sections[i] == section) {
		    this.sections.splice(i,1);
		    this.redraw();

		    return;
		}
	    }
	},

	zoomIn: function() {
	    //this.setDistance(this.getDistance() - this.stepdist);
	    this.setDistance(0.9 * (this.getDistance()-1) + 1);
	    this.events.triggerEvent('zoomend');
	},

	zoomOut: function() {
	    //this.setDistance(this.getDistance() + this.stepdist);
	    this.setDistance((this.getDistance()-1)/0.9 + 1);

	    this.events.triggerEvent('zoomend');
	},

	addControl: function(c) {
	    c.map = this;
	    var div = c.draw();
	    this.controls.push(c);

	    if (div) {
		this.viewPortDiv.appendChild(div);
		$(div).click(function() { c.trigger(); });
	    }
	}
    });



EarthGL.Grid = EarthGL.Class({
	initialize: function(tileSize) {
            this.tileSize = 256;
	    this.events = new EarthGL.Events(this);
	    this.numLoadingTiles = 0;
	},

	load: function(lon,lat) {

	}

    });

// image from tile WMS server


EarthGL.TileImages = EarthGL.Class({
        initialize: function(textureWidth,textureHeight,layer) {
            this.canvas = document.createElement("canvas");
            document.body.appendChild(this.canvas);
            this.canvas.style.borderStyle = "solid";

            this.width = textureWidth;
            this.height = textureHeight;

            this.canvas.width = textureWidth;
            this.canvas.height = textureHeight;
            this.ctx = this.canvas.getContext("2d");
            //this.ctx.translate(0,-this.canvas.height);
            //this.ctx.scale(1,-1);
            this.tileSize = 256;
            //this.tileSize = 32;
            this.tileSize = Math.min(textureWidth,this.tileSize);
            this.tileSize = Math.min(textureHeight,this.tileSize);
            //this.tileSize = 128;
            this.layer = layer;
            this.img = [];
            this.tiles = [];
	    this.numLoadingTiles = 0;

	    // resolution level (index from table resolutions)
	    this.resLevel = 0;
            // event handler
            this.events = new EarthGL.Events(this);

        },


        draw: function(lon,lat) {
            //var lon = [-180, 180], lat = [-90, 90];
            var that = this;
	    // available resolutions degree/pixel
	    //  360 ./ (2.^[8:16])
	    
	    var resolutions = [22.5, 11.25, 5.625, 2.8125, 
			       1.40625,  0.703125,  0.3515625,  0.17578125,   0.087890625,   0.0439453125, 
			       0.02197265625,   0.010986328125,   0.0054931640625];


            function projection(lon2,lat2) {
		//var ires = 1/resolutions[that.resLevel];

		var iresx = that.canvas.width/(lon[1]-lon[0]);
		var iresy = that.canvas.height/(lat[1]-lat[0]);

                var t = {x: (lon2 - lon[0])*iresx,
                         y: (lat[1] - lat2)*iresy};
                return t;
            }


            var urls = [this.layer.getFullRequestString()], t, newParams;
            var that = this;

            var fun = function() {
                var img = this;
                var t = that.tiles.filter(function (t) { return t.img == img; })[0];

                //that.ctx.drawImage(t.img,t.x[0],t.y[0]);
                //that.ctx.drawImage(t.img,t.x[0],t.y[0],that.tileSize,that.tileSize*0.8);
                that.ctx.drawImage(t.img,t.x[0],t.y[0],that.tileSize*that.scalex,that.tileSize*that.scaley);

		// decrement numLoadingTiles
		that.numLoadingTiles -= 1;

		that.events.triggerEvent('tileloaded',that);

		if (that.numLoadingTiles == 0) {
		    that.events.triggerEvent('loaded',that);
		}
            };                        
        

	    // target resolution
	    var res = (lon[1]-lon[0])  / this.canvas.width;

	    // search for matching resolution
	    for (this.resLevel=0; this.resLevel < resolutions.length-1; this.resLevel++) { 
		if (res >= resolutions[this.resLevel]) { 
		    break; 
		} 
	    }; 

	    //this.resLevel = 1;
	    console.log('res',res,resolutions[this.resLevel]);

            this.tiles = [];
            //var dlon = (lon[1]-lon[0]) * this.tileSize / this.canvas.width;
            //var dlat = (lat[1]-lat[0]) * this.tileSize / this.canvas.height;

	    var dlon = resolutions[this.resLevel]*this.tileSize;
	    var dlat = resolutions[this.resLevel]*this.tileSize;

            var slon = Math.floor(lon[0]/dlon)*dlon;
            var slat = Math.floor(lat[0]/dlat)*dlat;
            var x,y;

	    // scale image such that the resolution matches
	    this.scalex = (resolutions[this.resLevel] * this.canvas.width)/(lon[1]-lon[0]);
	    this.scaley = (resolutions[this.resLevel] * this.canvas.height)/(lat[1]-lat[0]);

	    //this.ctx.scale(this.scalex,this.scaley);

	    this.numLoadingTiles = 0;

            for (var ilon = slon; ilon < lon[1]; ilon = ilon + dlon) {
                for (var ilat = slat; ilat < lat[1]; ilat = ilat + dlat) {
                    t = {lon: [], lat: [], x: [], y: []};
                    t.lon[0] = ilon;
                    t.lon[1] = ilon+dlon;
                    t.lat[0] = ilat;
                    t.lat[1] = ilat+dlat;

                    var ll = projection(t.lon[0],t.lat[0]);
                    var ur = projection(t.lon[1],t.lat[1]);

                    t.x = [ll.x, ur.x];
                    t.y = [ur.y, ll.y];

                    newParams = {bbox: t.lon[0] + ',' + t.lat[0] + ',' + t.lon[1] + ',' + t.lat[1],
                                 width: this.tileSize,
                                 height: this.tileSize
                    };


		    this.numLoadingTiles += 1;

                    t.img = new Image();
                    t.img.onload = fun;
                    t.img.src = this.layer.getFullRequestString(newParams);

		    //console.log('src',t.img.src);
		    console.log('bbox ',newParams.bbox);
                    this.tiles.push(t);
                }
            }

        },

        data: function() {
	    return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);	    
	}
    });
