'use strict';

var createMesh = require('../../../stackgl_modules').gl_mesh3d;
var parseColorScale = require('../../lib/gl_format_color').parseColorScale;
var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray;
var str2RgbaArray = require('../../lib/str2rgbarray');
var extractOpts = require('../../components/colorscale').extractOpts;
var zip3 = require('../../plots/gl3d/zip3');

var findNearestOnAxis = function(w, arr) {
    for(var q = arr.length - 1; q > 0; q--) {
        var min = Math.min(arr[q], arr[q - 1]);
        var max = Math.max(arr[q], arr[q - 1]);
        if(max > min && min < w && w <= max) {
            return {
                id: q,
                distRatio: (max - w) / (max - min)
            };
        }
    }
    return {
        id: 0,
        distRatio: 0
    };
};

function IsosurfaceTrace(scene, mesh, uid) {
    this.scene = scene;
    this.uid = uid;
    this.mesh = mesh;
    this.name = '';
    this.data = null;
    this.showContour = false;
}

var proto = IsosurfaceTrace.prototype;

proto.handlePick = function(selection) {
    if(selection.object === this.mesh) {
        var rawId = selection.data.index;

        var x = this.data._meshX[rawId];
        var y = this.data._meshY[rawId];
        var z = this.data._meshZ[rawId];

        var height = this.data._Ys.length;
        var depth = this.data._Zs.length;

        var i = findNearestOnAxis(x, this.data._Xs).id;
        var j = findNearestOnAxis(y, this.data._Ys).id;
        var k = findNearestOnAxis(z, this.data._Zs).id;

        var selectIndex = selection.index = k + depth * j + depth * height * i;

        selection.traceCoordinate = [
            this.data._meshX[selectIndex],
            this.data._meshY[selectIndex],
            this.data._meshZ[selectIndex],
            this.data._value[selectIndex]
        ];

        var text = this.data.hovertext || this.data.text;
        if(isArrayOrTypedArray(text) && text[selectIndex] !== undefined) {
            selection.textLabel = text[selectIndex];
        } else if(text) {
            selection.textLabel = text;
        }

        return true;
    }
};

proto.update = function(data) {
    var scene = this.scene;
    var layout = scene.fullSceneLayout;

    this.data = generateIsoMeshes(data);

    // Unpack position data
    function toDataCoords(axis, coord, scale, calendar) {
        return coord.map(function(x) {
            return axis.d2l(x, 0, calendar) * scale;
        });
    }

    var positions = zip3(
        toDataCoords(layout.xaxis, data._meshX, scene.dataScale[0], data.xcalendar),
        toDataCoords(layout.yaxis, data._meshY, scene.dataScale[1], data.ycalendar),
        toDataCoords(layout.zaxis, data._meshZ, scene.dataScale[2], data.zcalendar));

    var cells = zip3(data._meshI, data._meshJ, data._meshK);

    var config = {
        positions: positions,
        cells: cells,
        lightPosition: [data.lightposition.x, data.lightposition.y, data.lightposition.z],
        ambient: data.lighting.ambient,
        diffuse: data.lighting.diffuse,
        specular: data.lighting.specular,
        roughness: data.lighting.roughness,
        fresnel: data.lighting.fresnel,
        vertexNormalsEpsilon: data.lighting.vertexnormalsepsilon,
        faceNormalsEpsilon: data.lighting.facenormalsepsilon,
        opacity: data.opacity,
        contourEnable: data.contour.show,
        contourColor: str2RgbaArray(data.contour.color).slice(0, 3),
        contourWidth: data.contour.width,
        useFacetNormals: data.flatshading
    };

    var cOpts = extractOpts(data);
    config.vertexIntensity = data._meshIntensity;
    config.vertexIntensityBounds = [cOpts.min, cOpts.max];
    config.colormap = parseColorScale(data);

    // Update mesh
    this.mesh.update(config);
};

proto.dispose = function() {
    this.scene.glplot.remove(this.mesh);
    this.mesh.dispose();
};

var GRID_TYPES = ['xyz', 'xzy', 'yxz', 'yzx', 'zxy', 'zyx'];

function generateIsoMeshes(data) {
    data._meshI = [];
    data._meshJ = [];
    data._meshK = [];

    var showSurface = data.surface.show;
    var showSpaceframe = data.spaceframe.show;

    var surfaceFill = data.surface.fill;
    var spaceframeFill = data.spaceframe.fill;

    var drawingSurface = false;
    var drawingSpaceframe = false;

    var numFaces = 0;
    var numVertices;
    var beginVertextLength;

    var Xs = data._Xs;
    var Ys = data._Ys;
    var Zs = data._Zs;

    var width = Xs.length;
    var height = Ys.length;
    var depth = Zs.length;

    var filled = GRID_TYPES.indexOf(data._gridFill.replace(/-/g, '').replace(/\+/g, ''));

    var getIndex = function(i, j, k) {
        switch(filled) {
            case 5: // 'zyx'
                return k + depth * j + depth * height * i;
            case 4: // 'zxy'
                return k + depth * i + depth * width * j;
            case 3: // 'yzx'
                return j + height * k + height * depth * i;
            case 2: // 'yxz'
                return j + height * i + height * width * k;
            case 1: // 'xzy'
                return i + width * k + width * depth * j;
            default: // case 0: // 'xyz'
                return i + width * j + width * height * k;
        }
    };

    var minValues = data._minValues;
    var maxValues = data._maxValues;

    var vMin = data._vMin;
    var vMax = data._vMax;

    var allXs;
    var allYs;
    var allZs;
    var allVs;

    function findVertexId(x, y, z) {
        // could be used to find the vertex id of previously generated vertex within the group

        var len = allVs.length;
        for(var f = beginVertextLength; f < len; f++) {
            if(
                x === allXs[f] &&
                y === allYs[f] &&
                z === allZs[f]
            ) {
                return f;
            }
        }
        return -1;
    }

    function beginGroup() {
        beginVertextLength = numVertices;
    }

    function emptyVertices() {
        allXs = [];
        allYs = [];
        allZs = [];
        allVs = [];
        numVertices = 0;

        beginGroup();
    }

    function addVertex(x, y, z, v) {
        allXs.push(x);
        allYs.push(y);
        allZs.push(z);
        allVs.push(v);
        numVertices++;

        return numVertices - 1;
    }

    function addFace(a, b, c) {
        data._meshI.push(a);
        data._meshJ.push(b);
        data._meshK.push(c);
        numFaces++;

        return numFaces - 1;
    }

    function getCenter(A, B, C) {
        var M = [];
        for(var i = 0; i < A.length; i++) {
            M[i] = (A[i] + B[i] + C[i]) / 3.0;
        }
        return M;
    }

    function getBetween(A, B, r) {
        var M = [];
        for(var i = 0; i < A.length; i++) {
            M[i] = A[i] * (1 - r) + r * B[i];
        }
        return M;
    }

    var activeFill;
    function setFill(fill) {
        activeFill = fill;
    }

    function createOpenTri(xyzv, abc) {
        var A = xyzv[0];
        var B = xyzv[1];
        var C = xyzv[2];
        var G = getCenter(A, B, C);

        var r = Math.sqrt(1 - activeFill);
        var p1 = getBetween(G, A, r);
        var p2 = getBetween(G, B, r);
        var p3 = getBetween(G, C, r);

        var a = abc[0];
        var b = abc[1];
        var c = abc[2];

        return {
            xyzv: [
                [A, B, p2], [p2, p1, A],
                [B, C, p3], [p3, p2, B],
                [C, A, p1], [p1, p3, C]
            ],
            abc: [
                [a, b, -1], [-1, -1, a],
                [b, c, -1], [-1, -1, b],
                [c, a, -1], [-1, -1, c]
            ]
        };
    }

    function styleIncludes(style, char) {
        if(style === 'all' || style === null) return true;
        return (style.indexOf(char) > -1);
    }

    function mapValue(style, value) {
        if(style === null) return value;
        return style;
    }

    function drawTri(style, xyzv, abc) {
        beginGroup();

        var allXYZVs = [xyzv];
        var allABCs = [abc];
        if(activeFill >= 1) {
            allXYZVs = [xyzv];
            allABCs = [abc];
        } else if(activeFill > 0) {
            var openTri = createOpenTri(xyzv, abc);
            allXYZVs = openTri.xyzv;
            allABCs = openTri.abc;
        }

        for(var f = 0; f < allXYZVs.length; f++) {
            xyzv = allXYZVs[f];
            abc = allABCs[f];

            var pnts = [];
            for(var i = 0; i < 3; i++) {
                var x = xyzv[i][0];
                var y = xyzv[i][1];
                var z = xyzv[i][2];
                var v = xyzv[i][3];

                var id = (abc[i] > -1) ? abc[i] : findVertexId(x, y, z);
                if(id > -1) {
                    pnts[i] = id;
                } else {
                    pnts[i] = addVertex(x, y, z, mapValue(style, v));
                }
            }

            addFace(pnts[0], pnts[1], pnts[2]);
        }
    }

    function drawQuad(style, xyzv, abcd) {
        var makeTri = function(i, j, k) {
            drawTri(style, [xyzv[i], xyzv[j], xyzv[k]], [abcd[i], abcd[j], abcd[k]]);
        };

        makeTri(0, 1, 2);
        makeTri(2, 3, 0);
    }

    function drawTetra(style, xyzv, abcd) {
        var makeTri = function(i, j, k) {
            drawTri(style, [xyzv[i], xyzv[j], xyzv[k]], [abcd[i], abcd[j], abcd[k]]);
        };

        makeTri(0, 1, 2);
        makeTri(3, 0, 1);
        makeTri(2, 3, 0);
        makeTri(1, 2, 3);
    }

    function calcIntersection(pointOut, pointIn, min, max) {
        var value = pointOut[3];

        if(value < min) value = min;
        if(value > max) value = max;

        var ratio = (pointOut[3] - value) / (pointOut[3] - pointIn[3] + 0.000000001); // we had to add this error to force solve the tiny caps

        var result = [];
        for(var s = 0; s < 4; s++) {
            result[s] = (1 - ratio) * pointOut[s] + ratio * pointIn[s];
        }
        return result;
    }

    function inRange(value, min, max) {
        return (
            value >= min &&
            value <= max
        );
    }

    function almostInFinalRange(value) {
        var vErr = 0.001 * (vMax - vMin);
        return (
            value >= vMin - vErr &&
            value <= vMax + vErr
        );
    }

    function getXYZV(indecies) {
        var xyzv = [];
        for(var q = 0; q < 4; q++) {
            var index = indecies[q];
            xyzv.push(
                [
                    data._x[index],
                    data._y[index],
                    data._z[index],
                    data._value[index]
                ]
            );
        }

        return xyzv;
    }

    var MAX_PASS = 3;

    function tryCreateTri(style, xyzv, abc, min, max, nPass) {
        if(!nPass) nPass = 1;

        abc = [-1, -1, -1]; // Note: for the moment we override indices
        // to run faster! But it is possible to comment this line
        // to reduce the number of vertices.

        var result = false;

        var ok = [
            inRange(xyzv[0][3], min, max),
            inRange(xyzv[1][3], min, max),
            inRange(xyzv[2][3], min, max)
        ];

        if(!ok[0] && !ok[1] && !ok[2]) {
            return false;
        }

        var tryDrawTri = function(style, xyzv, abc) {
            if( // we check here if the points are in `real` iso-min/max range
                almostInFinalRange(xyzv[0][3]) &&
                almostInFinalRange(xyzv[1][3]) &&
                almostInFinalRange(xyzv[2][3])
            ) {
                drawTri(style, xyzv, abc);
                return true;
            } else if(nPass < MAX_PASS) {
                return tryCreateTri(style, xyzv, abc, vMin, vMax, ++nPass); // i.e. second pass using actual vMin vMax bounds
            }
            return false;
        };

        if(ok[0] && ok[1] && ok[2]) {
            return tryDrawTri(style, xyzv, abc) || result;
        }

        var interpolated = false;

        [
            [0, 1, 2],
            [2, 0, 1],
            [1, 2, 0]
        ].forEach(function(e) {
            if(ok[e[0]] && ok[e[1]] && !ok[e[2]]) {
                var A = xyzv[e[0]];
                var B = xyzv[e[1]];
                var C = xyzv[e[2]];

                var p1 = calcIntersection(C, A, min, max);
                var p2 = calcIntersection(C, B, min, max);

                result = tryDrawTri(style, [p2, p1, A], [-1, -1, abc[e[0]]]) || result;
                result = tryDrawTri(style, [A, B, p2], [abc[e[0]], abc[e[1]], -1]) || result;

                interpolated = true;
            }
        });
        if(interpolated) return result;

        [
            [0, 1, 2],
            [1, 2, 0],
            [2, 0, 1]
        ].forEach(function(e) {
            if(ok[e[0]] && !ok[e[1]] && !ok[e[2]]) {
                var A = xyzv[e[0]];
                var B = xyzv[e[1]];
                var C = xyzv[e[2]];

                var p1 = calcIntersection(B, A, min, max);
                var p2 = calcIntersection(C, A, min, max);

                result = tryDrawTri(style, [p2, p1, A], [-1, -1, abc[e[0]]]) || result;

                interpolated = true;
            }
        });
        return result;
    }

    function tryCreateTetra(style, abcd, min, max) {
        var result = false;

        var xyzv = getXYZV(abcd);

        var ok = [
            inRange(xyzv[0][3], min, max),
            inRange(xyzv[1][3], min, max),
            inRange(xyzv[2][3], min, max),
            inRange(xyzv[3][3], min, max)
        ];

        if(!ok[0] && !ok[1] && !ok[2] && !ok[3]) {
            return result;
        }

        if(ok[0] && ok[1] && ok[2] && ok[3]) {
            if(drawingSpaceframe) {
                result = drawTetra(style, xyzv, abcd) || result;
            }
            return result;
        }

        var interpolated = false;

        [
            [0, 1, 2, 3],
            [3, 0, 1, 2],
            [2, 3, 0, 1],
            [1, 2, 3, 0]
        ].forEach(function(e) {
            if(ok[e[0]] && ok[e[1]] && ok[e[2]] && !ok[e[3]]) {
                var A = xyzv[e[0]];
                var B = xyzv[e[1]];
                var C = xyzv[e[2]];
                var D = xyzv[e[3]];

                if(drawingSpaceframe) {
                    result = drawTri(style, [A, B, C], [abcd[e[0]], abcd[e[1]], abcd[e[2]]]) || result;
                } else {
                    var p1 = calcIntersection(D, A, min, max);
                    var p2 = calcIntersection(D, B, min, max);
                    var p3 = calcIntersection(D, C, min, max);

                    result = drawTri(null, [p1, p2, p3], [-1, -1, -1]) || result;
                }

                interpolated = true;
            }
        });
        if(interpolated) return result;

        [
            [0, 1, 2, 3],
            [1, 2, 3, 0],
            [2, 3, 0, 1],
            [3, 0, 1, 2],
            [0, 2, 3, 1],
            [1, 3, 2, 0]
        ].forEach(function(e) {
            if(ok[e[0]] && ok[e[1]] && !ok[e[2]] && !ok[e[3]]) {
                var A = xyzv[e[0]];
                var B = xyzv[e[1]];
                var C = xyzv[e[2]];
                var D = xyzv[e[3]];

                var p1 = calcIntersection(C, A, min, max);
                var p2 = calcIntersection(C, B, min, max);
                var p3 = calcIntersection(D, B, min, max);
                var p4 = calcIntersection(D, A, min, max);

                if(drawingSpaceframe) {
                    result = drawTri(style, [A, p4, p1], [abcd[e[0]], -1, -1]) || result;
                    result = drawTri(style, [B, p2, p3], [abcd[e[1]], -1, -1]) || result;
                } else {
                    result = drawQuad(null, [p1, p2, p3, p4], [-1, -1, -1, -1]) || result;
                }

                interpolated = true;
            }
        });
        if(interpolated) return result;

        [
            [0, 1, 2, 3],
            [1, 2, 3, 0],
            [2, 3, 0, 1],
            [3, 0, 1, 2]
        ].forEach(function(e) {
            if(ok[e[0]] && !ok[e[1]] && !ok[e[2]] && !ok[e[3]]) {
                var A = xyzv[e[0]];
                var B = xyzv[e[1]];
                var C = xyzv[e[2]];
                var D = xyzv[e[3]];

                var p1 = calcIntersection(B, A, min, max);
                var p2 = calcIntersection(C, A, min, max);
                var p3 = calcIntersection(D, A, min, max);

                if(drawingSpaceframe) {
                    result = drawTri(style, [A, p1, p2], [abcd[e[0]], -1, -1]) || result;
                    result = drawTri(style, [A, p2, p3], [abcd[e[0]], -1, -1]) || result;
                    result = drawTri(style, [A, p3, p1], [abcd[e[0]], -1, -1]) || result;
                } else {
                    result = drawTri(null, [p1, p2, p3], [-1, -1, -1]) || result;
                }

                interpolated = true;
            }
        });
        return result;
    }

    function addCube(style, p000, p001, p010, p011, p100, p101, p110, p111, min, max) {
        var result = false;

        if(drawingSurface) {
            if(styleIncludes(style, 'A')) {
                result = tryCreateTetra(null, [p000, p001, p010, p100], min, max) || result;
            }
            if(styleIncludes(style, 'B')) {
                result = tryCreateTetra(null, [p001, p010, p011, p111], min, max) || result;
            }
            if(styleIncludes(style, 'C')) {
                result = tryCreateTetra(null, [p001, p100, p101, p111], min, max) || result;
            }
            if(styleIncludes(style, 'D')) {
                result = tryCreateTetra(null, [p010, p100, p110, p111], min, max) || result;
            }
            if(styleIncludes(style, 'E')) {
                result = tryCreateTetra(null, [p001, p010, p100, p111], min, max) || result;
            }
        }

        if(drawingSpaceframe) {
            result = tryCreateTetra(style, [p001, p010, p100, p111], min, max) || result;
        }

        return result;
    }

    function addRect(style, a, b, c, d, min, max, previousResult) {
        return [
            (previousResult[0] === true) ? true :
            tryCreateTri(style, getXYZV([a, b, c]), [a, b, c], min, max),
            (previousResult[1] === true) ? true :
            tryCreateTri(style, getXYZV([c, d, a]), [c, d, a], min, max)
        ];
    }

    function begin2dCell(style, p00, p01, p10, p11, min, max, isEven, previousResult) {
        // used to create caps and/or slices on exact axis points
        if(isEven) {
            return addRect(style, p00, p01, p11, p10, min, max, previousResult);
        } else {
            return addRect(style, p01, p11, p10, p00, min, max, previousResult);
        }
    }

    function beginSection(style, i, j, k, min, max, distRatios) {
        // used to create slices between axis points

        var result = false;
        var A, B, C, D;

        var makeSection = function() {
            result = tryCreateTri(style, [A, B, C], [-1, -1, -1], min, max) || result;
            result = tryCreateTri(style, [C, D, A], [-1, -1, -1], min, max) || result;
        };

        var rX = distRatios[0];
        var rY = distRatios[1];
        var rZ = distRatios[2];

        if(rX) {
            A = getBetween(getXYZV([getIndex(i, j - 0, k - 0)])[0], getXYZV([getIndex(i - 1, j - 0, k - 0)])[0], rX);
            B = getBetween(getXYZV([getIndex(i, j - 0, k - 1)])[0], getXYZV([getIndex(i - 1, j - 0, k - 1)])[0], rX);
            C = getBetween(getXYZV([getIndex(i, j - 1, k - 1)])[0], getXYZV([getIndex(i - 1, j - 1, k - 1)])[0], rX);
            D = getBetween(getXYZV([getIndex(i, j - 1, k - 0)])[0], getXYZV([getIndex(i - 1, j - 1, k - 0)])[0], rX);
            makeSection();
        }

        if(rY) {
            A = getBetween(getXYZV([getIndex(i - 0, j, k - 0)])[0], getXYZV([getIndex(i - 0, j - 1, k - 0)])[0], rY);
            B = getBetween(getXYZV([getIndex(i - 0, j, k - 1)])[0], getXYZV([getIndex(i - 0, j - 1, k - 1)])[0], rY);
            C = getBetween(getXYZV([getIndex(i - 1, j, k - 1)])[0], getXYZV([getIndex(i - 1, j - 1, k - 1)])[0], rY);
            D = getBetween(getXYZV([getIndex(i - 1, j, k - 0)])[0], getXYZV([getIndex(i - 1, j - 1, k - 0)])[0], rY);
            makeSection();
        }

        if(rZ) {
            A = getBetween(getXYZV([getIndex(i - 0, j - 0, k)])[0], getXYZV([getIndex(i - 0, j - 0, k - 1)])[0], rZ);
            B = getBetween(getXYZV([getIndex(i - 0, j - 1, k)])[0], getXYZV([getIndex(i - 0, j - 1, k - 1)])[0], rZ);
            C = getBetween(getXYZV([getIndex(i - 1, j - 1, k)])[0], getXYZV([getIndex(i - 1, j - 1, k - 1)])[0], rZ);
            D = getBetween(getXYZV([getIndex(i - 1, j - 0, k)])[0], getXYZV([getIndex(i - 1, j - 0, k - 1)])[0], rZ);
            makeSection();
        }

        return result;
    }

    function begin3dCell(style, p000, p001, p010, p011, p100, p101, p110, p111, min, max, isEven) {
        // used to create spaceframe and/or iso-surfaces

        var cellStyle = style;
        if(isEven) {
            if(drawingSurface && style === 'even') cellStyle = null;
            return addCube(cellStyle, p000, p001, p010, p011, p100, p101, p110, p111, min, max);
        } else {
            if(drawingSurface && style === 'odd') cellStyle = null;
            return addCube(cellStyle, p111, p110, p101, p100, p011, p010, p001, p000, min, max);
        }
    }

    function draw2dX(style, items, min, max, previousResult) {
        var result = [];
        var n = 0;
        for(var q = 0; q < items.length; q++) {
            var i = items[q];
            for(var k = 1; k < depth; k++) {
                for(var j = 1; j < height; j++) {
                    result.push(
                        begin2dCell(style,
                            getIndex(i, j - 1, k - 1),
                            getIndex(i, j - 1, k),
                            getIndex(i, j, k - 1),
                            getIndex(i, j, k),
                            min,
                            max,
                            (i + j + k) % 2,
                            (previousResult && previousResult[n]) ? previousResult[n] : []
                        )
                    );
                    n++;
                }
            }
        }
        return result;
    }

    function draw2dY(style, items, min, max, previousResult) {
        var result = [];
        var n = 0;
        for(var q = 0; q < items.length; q++) {
            var j = items[q];
            for(var i = 1; i < width; i++) {
                for(var k = 1; k < depth; k++) {
                    result.push(
                        begin2dCell(style,
                            getIndex(i - 1, j, k - 1),
                            getIndex(i, j, k - 1),
                            getIndex(i - 1, j, k),
                            getIndex(i, j, k),
                            min,
                            max,
                            (i + j + k) % 2,
                            (previousResult && previousResult[n]) ? previousResult[n] : []
                        )
                    );
                    n++;
                }
            }
        }
        return result;
    }

    function draw2dZ(style, items, min, max, previousResult) {
        var result = [];
        var n = 0;
        for(var q = 0; q < items.length; q++) {
            var k = items[q];
            for(var j = 1; j < height; j++) {
                for(var i = 1; i < width; i++) {
                    result.push(
                        begin2dCell(style,
                            getIndex(i - 1, j - 1, k),
                            getIndex(i - 1, j, k),
                            getIndex(i, j - 1, k),
                            getIndex(i, j, k),
                            min,
                            max,
                            (i + j + k) % 2,
                            (previousResult && previousResult[n]) ? previousResult[n] : []
                        )
                    );
                    n++;
                }
            }
        }
        return result;
    }

    function draw3d(style, min, max) {
        for(var k = 1; k < depth; k++) {
            for(var j = 1; j < height; j++) {
                for(var i = 1; i < width; i++) {
                    begin3dCell(style,
                        getIndex(i - 1, j - 1, k - 1),
                        getIndex(i - 1, j - 1, k),
                        getIndex(i - 1, j, k - 1),
                        getIndex(i - 1, j, k),
                        getIndex(i, j - 1, k - 1),
                        getIndex(i, j - 1, k),
                        getIndex(i, j, k - 1),
                        getIndex(i, j, k),
                        min,
                        max,
                        (i + j + k) % 2
                    );
                }
            }
        }
    }

    function drawSpaceframe(style, min, max) {
        drawingSpaceframe = true;
        draw3d(style, min, max);
        drawingSpaceframe = false;
    }

    function drawSurface(style, min, max) {
        drawingSurface = true;
        draw3d(style, min, max);
        drawingSurface = false;
    }

    function drawSectionX(style, items, min, max, distRatios, previousResult) {
        var result = [];
        var n = 0;
        for(var q = 0; q < items.length; q++) {
            var i = items[q];
            for(var k = 1; k < depth; k++) {
                for(var j = 1; j < height; j++) {
                    result.push(
                        beginSection(style, i, j, k, min, max, distRatios[q],
                            (previousResult && previousResult[n]) ? previousResult[n] : []
                        )
                    );
                    n++;
                }
            }
        }
        return result;
    }

    function drawSectionY(style, items, min, max, distRatios, previousResult) {
        var result = [];
        var n = 0;
        for(var q = 0; q < items.length; q++) {
            var j = items[q];
            for(var i = 1; i < width; i++) {
                for(var k = 1; k < depth; k++) {
                    result.push(
                        beginSection(style, i, j, k, min, max, distRatios[q],
                            (previousResult && previousResult[n]) ? previousResult[n] : []
                        )
                    );
                    n++;
                }
            }
        }
        return result;
    }

    function drawSectionZ(style, items, min, max, distRatios, previousResult) {
        var result = [];
        var n = 0;
        for(var q = 0; q < items.length; q++) {
            var k = items[q];
            for(var j = 1; j < height; j++) {
                for(var i = 1; i < width; i++) {
                    result.push(
                        beginSection(style, i, j, k, min, max, distRatios[q],
                            (previousResult && previousResult[n]) ? previousResult[n] : []
                        )
                    );
                    n++;
                }
            }
        }
        return result;
    }

    function createRange(a, b) {
        var range = [];
        for(var q = a; q < b; q++) {
            range.push(q);
        }
        return range;
    }

    function insertGridPoints() {
        for(var i = 0; i < width; i++) {
            for(var j = 0; j < height; j++) {
                for(var k = 0; k < depth; k++) {
                    var index = getIndex(i, j, k);
                    addVertex(
                        data._x[index],
                        data._y[index],
                        data._z[index],
                        data._value[index]
                    );
                }
            }
        }
    }

    function drawAll() {
        emptyVertices();

        // insert grid points
        insertGridPoints();

        var activeStyle = null;

        // draw spaceframes
        if(showSpaceframe && spaceframeFill) {
            setFill(spaceframeFill);

            drawSpaceframe(activeStyle, vMin, vMax);
        }

        // draw iso-surfaces
        if(showSurface && surfaceFill) {
            setFill(surfaceFill);

            var surfacePattern = data.surface.pattern;
            var surfaceCount = data.surface.count;
            for(var q = 0; q < surfaceCount; q++) {
                var ratio = (surfaceCount === 1) ? 0.5 : q / (surfaceCount - 1);
                var level = (1 - ratio) * vMin + ratio * vMax;

                var d1 = Math.abs(level - minValues);
                var d2 = Math.abs(level - maxValues);
                var ranges = (d1 > d2) ?
                    [minValues, level] :
                    [level, maxValues];

                drawSurface(surfacePattern, ranges[0], ranges[1]);
            }
        }

        var setupMinMax = [
            [ Math.min(vMin, maxValues), Math.max(vMin, maxValues) ],
            [ Math.min(minValues, vMax), Math.max(minValues, vMax) ]
        ];

        ['x', 'y', 'z'].forEach(function(e) {
            var preRes = [];
            for(var s = 0; s < setupMinMax.length; s++) {
                var count = 0;

                var activeMin = setupMinMax[s][0];
                var activeMax = setupMinMax[s][1];

                // draw slices
                var slice = data.slices[e];
                if(slice.show && slice.fill) {
                    setFill(slice.fill);

                    var exactIndices = [];
                    var ceilIndices = [];
                    var distRatios = [];
                    if(slice.locations.length) {
                        for(var q = 0; q < slice.locations.length; q++) {
                            var near = findNearestOnAxis(
                                slice.locations[q],
                                (e === 'x') ? Xs :
                                (e === 'y') ? Ys : Zs
                            );

                            if(near.distRatio === 0) {
                                exactIndices.push(near.id);
                            } else if(near.id > 0) {
                                ceilIndices.push(near.id);
                                if(e === 'x') {
                                    distRatios.push([near.distRatio, 0, 0]);
                                } else if(e === 'y') {
                                    distRatios.push([0, near.distRatio, 0]);
                                } else {
                                    distRatios.push([0, 0, near.distRatio]);
                                }
                            }
                        }
                    } else {
                        if(e === 'x') {
                            exactIndices = createRange(1, width - 1);
                        } else if(e === 'y') {
                            exactIndices = createRange(1, height - 1);
                        } else {
                            exactIndices = createRange(1, depth - 1);
                        }
                    }

                    if(ceilIndices.length > 0) {
                        if(e === 'x') {
                            preRes[count] = drawSectionX(activeStyle, ceilIndices, activeMin, activeMax, distRatios, preRes[count]);
                        } else if(e === 'y') {
                            preRes[count] = drawSectionY(activeStyle, ceilIndices, activeMin, activeMax, distRatios, preRes[count]);
                        } else {
                            preRes[count] = drawSectionZ(activeStyle, ceilIndices, activeMin, activeMax, distRatios, preRes[count]);
                        }
                        count++;
                    }

                    if(exactIndices.length > 0) {
                        if(e === 'x') {
                            preRes[count] = draw2dX(activeStyle, exactIndices, activeMin, activeMax, preRes[count]);
                        } else if(e === 'y') {
                            preRes[count] = draw2dY(activeStyle, exactIndices, activeMin, activeMax, preRes[count]);
                        } else {
                            preRes[count] = draw2dZ(activeStyle, exactIndices, activeMin, activeMax, preRes[count]);
                        }
                        count++;
                    }
                }

                // draw caps
                var cap = data.caps[e];
                if(cap.show && cap.fill) {
                    setFill(cap.fill);
                    if(e === 'x') {
                        preRes[count] = draw2dX(activeStyle, [0, width - 1], activeMin, activeMax, preRes[count]);
                    } else if(e === 'y') {
                        preRes[count] = draw2dY(activeStyle, [0, height - 1], activeMin, activeMax, preRes[count]);
                    } else {
                        preRes[count] = draw2dZ(activeStyle, [0, depth - 1], activeMin, activeMax, preRes[count]);
                    }
                    count++;
                }
            }
        });

        // remove vertices arrays (i.e. grid points) in case no face was created.
        if(numFaces === 0) {
            emptyVertices();
        }

        data._meshX = allXs;
        data._meshY = allYs;
        data._meshZ = allZs;
        data._meshIntensity = allVs;

        data._Xs = Xs;
        data._Ys = Ys;
        data._Zs = Zs;
    }

    drawAll();

    return data;
}

function createIsosurfaceTrace(scene, data) {
    var gl = scene.glplot.gl;
    var mesh = createMesh({gl: gl});
    var result = new IsosurfaceTrace(scene, mesh, data.uid);

    mesh._trace = result;
    result.update(data);
    scene.glplot.add(mesh);
    return result;
}

module.exports = {
    findNearestOnAxis: findNearestOnAxis,
    generateIsoMeshes: generateIsoMeshes,
    createIsosurfaceTrace: createIsosurfaceTrace,
};
