// Extend the Array class
Array.prototype.max = function() {
    return Math.max.apply(null, this);
};
Array.prototype.min = function() {
    return Math.min.apply(null, this);
};
Array.prototype.mean = function() {
    var i, sum;
    for(i=0,sum=0;i<this.length;i++)
	sum += this[i];
    return sum / this.length;
};
Array.prototype.pip = function(x, y) {
    var i, j, c = false;
    for(i=0,j=this.length-1;i<this.length;j=i++) {
	if( ((this[i][1]>y) != (this[j][1]>y)) &&
	    (x<(this[j][0]-this[i][0]) * (y-this[i][1]) / (this[j][1]-this[i][1]) + this[i][0]) ) {
	    c = !c;
	}
    }
    return c;
}

var kriging = function() {
    var kriging = {};

    var createArrayWithValues = function(value, n) {
        var array = [];
        for ( var i = 0; i < n; i++) {
            array.push(value);
        }
        return array;
    };

    // Matrix algebra
    kriging_matrix_diag = function(c, n) {
        var Z = createArrayWithValues(0, n * n);
        for(i=0;i<n;i++) Z[i*n+i] = c;
        return Z;
    };
    kriging_matrix_transpose = function(X, n, m) {
	var i, j, Z = Array(m*n);
	for(i=0;i<n;i++)
	    for(j=0;j<m;j++)
		Z[j*n+i] = X[i*m+j];
	return Z;
    };
    kriging_matrix_scale = function(X, c, n, m) {
	var i, j;
	for(i=0;i<n;i++)
	    for(j=0;j<m;j++)
		X[i*m+j] *= c;
    };
    kriging_matrix_add = function(X, Y, n, m) {
	var i, j, Z = Array(n*m);
	for(i=0;i<n;i++)
	    for(j=0;j<m;j++)
		Z[i*m+j] = X[i*m+j] + Y[i*m+j];
	return Z;
    };
    // Naive matrix multiplication
    kriging_matrix_multiply = function(X, Y, n, m, p) {
	var i, j, k, Z = Array(n*p);
	for(i=0;i<n;i++) {
	    for(j=0;j<p;j++) {
		Z[i*p+j] = 0;
		for(k=0;k<m;k++)
		    Z[i*p+j] += X[i*m+k]*Y[k*p+j];
	    }
	}
	return Z;
    };
    // Cholesky decomposition
    kriging_matrix_chol = function(X, n) {
	var i, j, k, sum, p = Array(n);
	for(i=0;i<n;i++) p[i] = X[i*n+i];
	for(i=0;i<n;i++) {
	    for(j=0;j<i;j++)
		p[i] -= X[i*n+j]*X[i*n+j];
	    if(p[i]<=0) return false;
	    p[i] = Math.sqrt(p[i]);
	    for(j=i+1;j<n;j++) {
		for(k=0;k<i;k++)
		    X[j*n+i] -= X[j*n+k]*X[i*n+k];
		X[j*n+i] /= p[i];
	    }
	}
	for(i=0;i<n;i++) X[i*n+i] = p[i];
	return true;
    };
    // Inversion of cholesky decomposition
    kriging_matrix_chol2inv = function(X, n) {
	var i, j, k, sum;
	for(i=0;i<n;i++) {
	    X[i*n+i] = 1/X[i*n+i];
	    for(j=i+1;j<n;j++) {
		sum = 0;
		for(k=i;k<j;k++)
		    sum -= X[j*n+k]*X[k*n+i];
		X[j*n+i] = sum/X[j*n+j];
	    }
	}
	for(i=0;i<n;i++)
	    for(j=i+1;j<n;j++)
		X[i*n+j] = 0;
	for(i=0;i<n;i++) {
	    X[i*n+i] *= X[i*n+i];
	    for(k=i+1;k<n;k++)
		X[i*n+i] += X[k*n+i]*X[k*n+i];
	    for(j=i+1;j<n;j++)
		for(k=j;k<n;k++)
		    X[i*n+j] += X[k*n+i]*X[k*n+j];
	}
	for(i=0;i<n;i++)
	    for(j=0;j<i;j++)
		X[i*n+j] = X[j*n+i];

    };
    // Inversion via gauss-jordan elimination
    kriging_matrix_solve = function(X, n) {
	var m = n;
	var b = Array(n*n);
	var indxc = Array(n);
	var indxr = Array(n);
	var ipiv = Array(n);
	var i, icol, irow, j, k, l, ll;
	var big, dum, pivinv, temp;

	for(i=0;i<n;i++)
	    for(j=0;j<n;j++) {
		if(i==j) b[i*n+j] = 1;
		else b[i*n+j] = 0;
	    }
	for(j=0;j<n;j++) ipiv[j] = 0;
	for(i=0;i<n;i++) {
	    big = 0;
	    for(j=0;j<n;j++) {
		if(ipiv[j]!=1) {
		    for(k=0;k<n;k++) {
			if(ipiv[k]==0) {
			    if(Math.abs(X[j*n+k])>=big) {
				big = Math.abs(X[j*n+k]);
				irow = j;
				icol = k;
			    }
			}
		    }
		}
	    }
	    ++(ipiv[icol]);

	    if(irow!=icol) {
		for(l=0;l<n;l++) {
		    temp = X[irow*n+l];
		    X[irow*n+l] = X[icol*n+l];
		    X[icol*n+l] = temp;
		}
		for(l=0;l<m;l++) {
		    temp = b[irow*n+l];
		    b[irow*n+l] = b[icol*n+l];
		    b[icol*n+l] = temp;
		}
	    }
	    indxr[i] = irow;
	    indxc[i] = icol;

	    if(X[icol*n+icol]==0) return false; // Singular

	    pivinv = 1 / X[icol*n+icol];
	    X[icol*n+icol] = 1;
	    for(l=0;l<n;l++) X[icol*n+l] *= pivinv;
	    for(l=0;l<m;l++) b[icol*n+l] *= pivinv;

	    for(ll=0;ll<n;ll++) {
		if(ll!=icol) {
		    dum = X[ll*n+icol];
		    X[ll*n+icol] = 0;
		    for(l=0;l<n;l++) X[ll*n+l] -= X[icol*n+l]*dum;
		    for(l=0;l<m;l++) b[ll*n+l] -= b[icol*n+l]*dum;
		}
	    }
	}
	for(l=(n-1);l>=0;l--)
	    if(indxr[l]!=indxc[l]) {
		for(k=0;k<n;k++) {
		    temp = X[k*n+indxr[l]];
		    X[k*n+indxr[l]] = X[k*n+indxc[l]];
		    X[k*n+indxc[l]] = temp;
		}
	    }

	return true;
    }

    // Variogram models
    kriging_variogram_gaussian = function(h, nugget, range, sill, A) {
	return nugget + ((sill-nugget)/range)*
	( 1.0 - Math.exp(-(1.0/A)*Math.pow(h/range, 2)) );
    };
    kriging_variogram_exponential = function(h, nugget, range, sill, A) {
	return nugget + ((sill-nugget)/range)*
	( 1.0 - Math.exp(-(1.0/A) * (h/range)) );
    };
    kriging_variogram_spherical = function(h, nugget, range, sill, A) {
	if(h>range) return nugget + (sill-nugget)/range;
	return nugget + ((sill-nugget)/range)*
	( 1.5*(h/range) - 0.5*Math.pow(h/range, 3) );
    };

    // Train using gaussian processes with bayesian priors
    kriging.train = function(t, x, y, model, sigma2, alpha) {
	var variogram = {
	    t      : t,
	    x      : x,
	    y      : y,
	    nugget : 0.0,
	    range  : 0.0,
	    sill   : 0.0,
	    A      : 1/3,
	    n      : 0
	};
	switch(model) {
	case "gaussian":
	    variogram.model = kriging_variogram_gaussian;
	    break;
	case "exponential":
	    variogram.model = kriging_variogram_exponential;
	    break;
	case "spherical":
	    variogram.model = kriging_variogram_spherical;
	    break;
	};

	// Lag distance/semivariance
	var i, j, k, l, n = t.length;
	var distance = Array((n*n-n)/2);
	for(i=0,k=0;i<n;i++)
	    for(j=0;j<i;j++,k++) {
		distance[k] = Array(2);
		distance[k][0] = Math.pow(
		    Math.pow(x[i]-x[j], 2)+
		    Math.pow(y[i]-y[j], 2), 0.5);
		distance[k][1] = Math.abs(t[i]-t[j]);
	    }
	distance.sort(function(a, b) { return a[0] - b[0]; });
	variogram.range = distance[(n*n-n)/2-1][0];

	// Bin lag distance
	var lags = ((n*n-n)/2)>30?30:(n*n-n)/2;
	var tolerance = variogram.range/lags;
	var lag = createArrayWithValues(0,lags);
	var semi = createArrayWithValues(0,lags);
	if(lags<30) {
	    for(l=0;l<lags;l++) {
		lag[l] = distance[l][0];
		semi[l] = distance[l][1];
	    }
	}
	else {
	    for(i=0,j=0,k=0,l=0;i<lags&&j<((n*n-n)/2);i++,k=0) {
		while( distance[j][0]<=((i+1)*tolerance) ) {
		    lag[l] += distance[j][0];
		    semi[l] += distance[j][1];
		    j++;k++;
		    if(j>=((n*n-n)/2)) break;
		}
		if(k>0) {
		    lag[l] /= k;
		    semi[l] /= k;
		    l++;
		}
	    }
	    if(l<2) return variogram; // Error: Not enough points
	}

	// Feature transformation
	n = l;
	variogram.range = lag[n-1]-lag[0];
	 var X = createArrayWithValues(1,2 * n);
	var Y = Array(n);
	var A = variogram.A;
	for(i=0;i<n;i++) {
	    switch(model) {
	    case "gaussian":
		X[i*2+1] = 1.0-Math.exp(-(1.0/A)*Math.pow(lag[i]/variogram.range, 2));
		break;
	    case "exponential":
		X[i*2+1] = 1.0-Math.exp(-(1.0/A)*lag[i]/variogram.range);
		break;
	    case "spherical":
		X[i*2+1] = 1.5*(lag[i]/variogram.range)-
		    0.5*Math.pow(lag[i]/variogram.range, 3);
		break;
	    };
	    Y[i] = semi[i];
	}

	// Least squares
	var Xt = kriging_matrix_transpose(X, n, 2);
	var Z = kriging_matrix_multiply(Xt, X, 2, n, 2);
	Z = kriging_matrix_add(Z, kriging_matrix_diag(1/alpha, 2), 2, 2);
	var cloneZ = Z.slice(0);
	if(kriging_matrix_chol(Z, 2))
	    kriging_matrix_chol2inv(Z, 2);
	else {
	    kriging_matrix_solve(cloneZ, 2);
	    Z = cloneZ;
	}
	var W = kriging_matrix_multiply(kriging_matrix_multiply(Z, Xt, 2, 2, n), Y, 2, n, 1);

	// Variogram parameters
	variogram.nugget = W[0];
	variogram.sill = W[1]*variogram.range+variogram.nugget;
	variogram.n = x.length;

	// Gram matrix with prior
	n = x.length;
	var K = Array(n*n);
	for(i=0;i<n;i++) {
	    for(j=0;j<i;j++) {
		K[i*n+j] = variogram.model(Math.pow(Math.pow(x[i]-x[j], 2)+
						    Math.pow(y[i]-y[j], 2), 0.5),
					   variogram.nugget,
					   variogram.range,
					   variogram.sill,
					   variogram.A);
		K[j*n+i] = K[i*n+j];
	    }
	    K[i*n+i] = variogram.model(0, variogram.nugget,
				       variogram.range,
				       variogram.sill,
				       variogram.A);
	}

	// Inverse penalized Gram matrix projected to target vector
	var C = kriging_matrix_add(K, kriging_matrix_diag(sigma2, n), n, n);
	var cloneC = C.slice(0);
	if(kriging_matrix_chol(C, n))
	    kriging_matrix_chol2inv(C, n);
	else {
	    kriging_matrix_solve(cloneC, n);
	    C = cloneC;
	}

	// Copy unprojected inverted matrix as K
	var K = C.slice(0);
	var M = kriging_matrix_multiply(C, t, n, n, 1);
	variogram.K = K;
	variogram.M = M;

	return variogram;
    };

    // Model prediction
    kriging.predict = function(x, y, variogram) {
	var i, k = Array(variogram.n);
	for(i=0;i<variogram.n;i++)
	    k[i] = variogram.model(Math.pow(Math.pow(x-variogram.x[i], 2)+
					    Math.pow(y-variogram.y[i], 2), 0.5),
				   variogram.nugget, variogram.range,
				   variogram.sill, variogram.A);
	return kriging_matrix_multiply(k, variogram.M, 1, variogram.n, 1)[0];
    };
    kriging.variance = function(x, y, variogram) {
	var i, k = Array(variogram.n);
	for(i=0;i<variogram.n;i++)
	    k[i] = variogram.model(Math.pow(Math.pow(x-variogram.x[i], 2)+
					    Math.pow(y-variogram.y[i], 2), 0.5),
				   variogram.nugget, variogram.range,
				   variogram.sill, variogram.A);
	return variogram.model(0, variogram.nugget, variogram.range,
			variogram.sill, variogram.A)+
	kriging_matrix_multiply(kriging_matrix_multiply(k, variogram.K,
							1, variogram.n, variogram.n),
				k, 1, variogram.n, 1)[0];
    };

    // Gridded matrices or contour paths
    kriging.grid = function(polygons, variogram, width) {
	var i, j, k, n = polygons.length;
	if(n==0) return;

	// Boundaries of polygons space
	var xlim = [polygons[0][0][0], polygons[0][0][0]];
	var ylim = [polygons[0][0][1], polygons[0][0][1]];
	for(i=0;i<n;i++) // Polygons
	    for(j=0;j<polygons[i].length;j++) { // Vertices
		if(polygons[i][j][0]<xlim[0])
		    xlim[0] = polygons[i][j][0];
		if(polygons[i][j][0]>xlim[1])
		    xlim[1] = polygons[i][j][0];
		if(polygons[i][j][1]<ylim[0])
		    ylim[0] = polygons[i][j][1];
		if(polygons[i][j][1]>ylim[1])
		    ylim[1] = polygons[i][j][1];
	    }

	// Alloc for O(n^2) space
	var xtarget, ytarget;
	var a = Array(2), b = Array(2);
	var lxlim = Array(2); // Local dimensions
	var lylim = Array(2); // Local dimensions
	var x = Math.ceil((xlim[1]-xlim[0])/width);
	var y = Math.ceil((ylim[1]-ylim[0])/width);

	var A = Array(x+1);
	for(i=0;i<=x;i++) A[i] = Array(y+1);
	for(i=0;i<n;i++) {
	    // Range for polygons[i]
	    lxlim[0] = polygons[i][0][0];
	    lxlim[1] = lxlim[0];
	    lylim[0] = polygons[i][0][1];
	    lylim[1] = lylim[0];
	    for(j=1;j<polygons[i].length;j++) { // Vertices
		if(polygons[i][j][0]<lxlim[0])
		    lxlim[0] = polygons[i][j][0];
		if(polygons[i][j][0]>lxlim[1])
		    lxlim[1] = polygons[i][j][0];
		if(polygons[i][j][1]<lylim[0])
		    lylim[0] = polygons[i][j][1];
		if(polygons[i][j][1]>lylim[1])
		    lylim[1] = polygons[i][j][1];
	    }

	    // Loop through polygon subspace
	    a[0] = Math.floor(((lxlim[0]-((lxlim[0]-xlim[0])%width)) - xlim[0])/width);
	    a[1] = Math.ceil(((lxlim[1]-((lxlim[1]-xlim[1])%width)) - xlim[0])/width);
	    b[0] = Math.floor(((lylim[0]-((lylim[0]-ylim[0])%width)) - ylim[0])/width);
	    b[1] = Math.ceil(((lylim[1]-((lylim[1]-ylim[1])%width)) - ylim[0])/width);
	    for(j=a[0];j<=a[1];j++)
		for(k=b[0];k<=b[1];k++) {
		    xtarget = xlim[0] + j*width;
		    ytarget = ylim[0] + k*width;
		    if(polygons[i].pip(xtarget, ytarget))
			A[j][k] = kriging.predict(xtarget,
						  ytarget,
						  variogram);
		}
	}
	A.xlim = xlim;
	A.ylim = ylim;
	A.zlim = [variogram.t.min(), variogram.t.max()];
	A.width = width;
	return A;
    };
    kriging.contour = function(value, polygons, variogram) {

    };

    // Plotting on the DOM
    kriging.plot = function(canvas, grid, xlim, ylim, colors) {
	// Clear screen
	var ctx = canvas.getContext("2d");
	ctx.clearRect(0, 0, canvas.width, canvas.height);

	// Starting boundaries
	var range = [xlim[1]-xlim[0], ylim[1]-ylim[0], grid.zlim[1]-grid.zlim[0]];
	var i, j, x, y, z;
	var n = grid.length;
	var m = grid[0].length;
	var wx = Math.ceil(grid.width*canvas.width/(xlim[1]-xlim[0]));
	var wy = Math.ceil(grid.width*canvas.height/(ylim[1]-ylim[0]));
	for(i=0;i<n;i++)
	    for(j=0;j<m;j++) {
		if(grid[i][j]==undefined) continue;
		x = canvas.width*(i*grid.width+grid.xlim[0]-xlim[0])/range[0];
		y = canvas.height*(1-(j*grid.width+grid.ylim[0]-ylim[0])/range[1]);
		z = (grid[i][j]-grid.zlim[0])/range[2];
		if(z<0.0) z = 0.0;
		if(z>1.0) z = 1.0;

		ctx.fillStyle = colors[Math.floor((colors.length-1)*z)];
		ctx.fillRect(Math.round(x-wx/2), Math.round(y-wy/2), wx, wy);
	    }

    };


    return kriging;
}();

if (module && module.exports){
    module.exports = kriging;
}