/**
 * Created by aghassaei on 1/16/15.
 */


Lattice = Backbone.Model.extend({

    defaults: {

        units: "mm",

        nodes: [],
        cells: [[[null]]],//3D matrix containing all cells and null, dynamic size
        cellsMin: {x:0, y:0, z:0},//min position of cells matrix
        cellsMax: {x:0, y:0, z:0},//max position of cells matrix
        numCells: 0,

        basePlane: null,//plane to build from
        scale: 20,
        highlighter: null,//highlights build-able surfaces

        //spacing for connectors/joints
        cellSeparation: {xy:0, z:0},

        cellType: "octa",
        connectionType: "face",
        partType: "triangle"
    },

    //pass in fillGeometry

    initialize: function(options){

        _.extend(this, OctaLatticeSubclasses, OtherLatticeSubclasses);

        //bind events
        this.listenTo(this, "change:scale", this._scaleDidChange);
        this.listenTo(options.appState, "change:cellMode", this._updateForMode);
        this.listenTo(this, "change:partType", this._updatePartType);
        this.listenTo(this, "change:cellType change:connectionType", this._updateLatticeType);
        this.listenTo(this, "change:cellSeparation", this._updateCellSeparation);
    },

    delayedInit: function(){
        this._updateLatticeType();
    },

    ////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////ADD/REMOVE CELLS/////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////

    addCellsInRange: function(range){//add a block of cells (extrude)
        var scale = this.get("scale");
        var cells = this.get("cells");
        this._checkForMatrixExpansion(cells, range.max, range.min, "cellsMax", "cellsMin");

        var cellsMin = this.get("cellsMin");
        var relativeMin = this._subtract(range.min, cellsMin);
        var relativeMax = this._subtract(range.max, this.get("cellsMin"));

        for (var x=relativeMin.x;x<=relativeMax.x;x++){
            for (var y=relativeMin.y;y<=relativeMax.y;y++){
                for (var z=relativeMin.z;z<=relativeMax.z;z++){
                    if (!cells[x][y][z]) {
                        cells[x][y][z] = this.makeCellForLatticeType(this._add({x:x, y:y, z:z}, cellsMin), scale);
                        this.set("numCells", this.get("numCells")+1);
                    } else console.warn("already a cell there");
                }
            }
        }
        dmaGlobals.three.render();
    },

    addCellAtIndex: function(indices){

        var scale = this.get("scale");
        var cells = this.get("cells");
        this._checkForMatrixExpansion(cells, indices, indices, "cellsMax", "cellsMin");

        var index = this._subtract(indices, this.get("cellsMin"));
        if (!cells[index.x][index.y][index.z]) {
            cells[index.x][index.y][index.z] = this.makeCellForLatticeType(indices, scale);
            this.set("numCells", this.get("numCells")+1);
            dmaGlobals.three.render();
        } else console.warn("already a cell there");

    },

    _indexForPosition: function(absPosition){
        var position = {};
        var scale = this.get("scale");
        position.x = Math.floor(absPosition.x/this.xScale(scale));
        position.y = Math.floor(absPosition.y/this.yScale(scale));
        position.z = Math.floor(absPosition.z/this.zScale(scale));
        return position;
    },

    _positionForIndex: function(index){
        var scale = this.get("scale");
        var position = _.clone(index);
        position.x = (position.x+0.5)*this.xScale(scale);
        position.y = (position.y+0.5)*this.yScale(scale);
        position.z = (position.z+0.5)*this.zScale(scale);
        return position;
    },

//    removeCellAtIndex: function(indices){
//
//        var index = this._subtract(indices, this.get("cellsMin"));
//        var cells = this.get("cells");
//        if (index.x<cells.length && index.y<cells[0].length && index.z<cells[0][0].length){
//            this.removeCell(cells[index.x][index.y][index.z]);
//        }
//    },

    removeCell: function(cell){
        if (!cell) return;
        var index = this._subtract(cell.indices, this.get("cellsMin"));
        var cells = this.get("cells");
        cell.destroy();
        cells[index.x][index.y][index.z] = null;

        //todo shrink cells matrix if needed

        this.set("numCells", this.get("numCells")-1);
        dmaGlobals.three.render();
    },

    //todo send clear all to three and destroy without sceneRemove to cell
    clearCells: function(){
        this._iterCells(this.get("cells"), function(cell){
            if (cell && cell.destroy) cell.destroy();
        });
        this.set("cells", [[[null]]]);
        this.set("cellsMax", {x:0, y:0, z:0});
        this.set("cellsMin", {x:0, y:0, z:0});
        this.set("nodes", []);
        this.set("numCells", 0);
        if (this.get("basePlane")) this.get("basePlane").set("zIndex", 0);
        dmaGlobals.three.render();
    },

    ////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////FILL GEOMETRY////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////

    subtractMesh: function(mesh){
        //todo this is specific to octa face

        var scale = this.get("scale");
        var xScale = this.xScale(scale);
        var yScale = this.yScale(scale);
        var zScale = this.zScale(scale);

        var cells = this.get("cells");
        var cellsMin = this.get("cellsMin");

        var allVertexPos = mesh.geometry.attributes.position.array;

        var zHeight = 0;
        for (var x=0;x<cells.length;x++){
            for (var y=0;y<cells[0].length;y++){
                var firstCell = null;
                for (var z=0;z<cells[0][0].length;z++){
                    firstCell = cells[x][y][z];
                    if (firstCell) break;
                }
                if (!firstCell) continue;//nothing in col

                var origin = this._positionForIndex(firstCell.indices);
//                    firstCell._calcPosition(0, this._add({x:x,y:y,z:z}, cellsMin));
                zHeight = this._findIntersectionsInWindow(xScale/2, yScale/2, origin, allVertexPos) || zHeight;

                zHeight = Math.floor(zHeight/zScale);
                for (var z=0;z<zHeight;z++){
                    var cell = cells[x][y][z];
                    if (cell) cell.destroy();
                    cells[x][y][z] = null;
                }
            }

        }
        dmaGlobals.three.render();
    },

    _findIntersectionsInWindow: function(windowX, windowY, origin, allVertexPos){
        for (var i=0;i<allVertexPos.length;i+=3){
            if (allVertexPos[i] > origin.x-windowX && allVertexPos[i] < origin.x+windowX
                && allVertexPos[i+1] > origin.y-windowY && allVertexPos[i+1] < origin.y+windowY){
                return allVertexPos[i+2];
            }
        }
        return null
    },


    ////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////CELLS ARRAY//////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////

    _checkForMatrixExpansion: function(cells, indicesMax, indicesMin, maxName, minName){

        var lastMax = this.get(maxName);
        var lastMin = this.get(minName);
        var newMax = this._updateCellsMax(indicesMax, lastMax);
        var newMin = this._updateCellsMin(indicesMin, lastMin);
        if (newMax) {
            this._expandCellsArray(cells, this._subtract(newMax, lastMax), false);
            this.set(maxName, newMax);
        }
        if (newMin) {
            this._expandCellsArray(cells, this._subtract(lastMin, newMin), true);
            this.set(minName, newMin);
        }
    },

    _expandCellsArray: function(cells, expansion, fromFront){

        _.each(_.keys(expansion), function(key){
            if (expansion[key] == 0) return;//no expansion on this axis

            var cellsX = cells.length;
            var cellsY = cells[0].length;
            var cellsZ = cells[0][0].length;

            if (key=="x"){
                for (var x=0;x<expansion[key];x++){
                    var newLayer = [];
                    for (var y=0;y<cellsY;y++){
                        var newCol = [];
                        for (var z=0;z<cellsZ;z++){
                            newCol.push(null);
                        }
                        newLayer.push(newCol);
                    }
                    if (fromFront) cells.unshift(newLayer);
                    else cells.push(newLayer);
                }
            } else if (key=="y"){
                for (var x=0;x<cellsX;x++){
                    for (var y=0;y<expansion[key];y++){
                        var newCol = [];
                        for (var z=0;z<cellsZ;z++){
                            newCol.push(null);
                        }
                        if (fromFront) cells[x].unshift(newCol);
                        else cells[x].push(newCol);
                    }
                }
            } else if (key=="z"){
                for (var x=0;x<cellsX;x++){
                    for (var y=0;y<cellsY;y++){
                        for (var z=0;z<expansion[key];z++){
                            if (fromFront) cells[x][y].unshift(null);
                            else cells[x][y].push(null);
                        }
                    }
                }
            }
        });
    },

    _updateCellsMin: function(newPosition, currentMin){
        var newMin = {};
        var hasChanged = false;
        _.each(_.keys(newPosition), function(key){
            if (newPosition[key]<currentMin[key]){
                hasChanged = true;
                newMin[key] = newPosition[key];
            } else {
                newMin[key] = currentMin[key];
            }
        });
        if (hasChanged) return newMin;
        return false;
    },

    _updateCellsMax: function(newPosition, currentMax){
        var newMax = {};
        var hasChanged = false;
        _.each(_.keys(newPosition), function(key){
            if (newPosition[key]>currentMax[key]){
                hasChanged = true;
                newMax[key] = newPosition[key];
            } else {
                newMax[key] = currentMax[key];
            }
        });
        if (hasChanged) return newMax;
        return false;
    },

    _subtract: function(pos1, pos2){
        return {x:pos1.x-pos2.x, y:pos1.y-pos2.y, z:pos1.z-pos2.z};
    },

    _add: function(pos1, pos2){
        return {x:pos1.x+pos2.x, y:pos1.y+pos2.y, z:pos1.z+pos2.z};
    },

    ////////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////EVENTS//////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////

    _updatePartType: function(){
        this._iterCells(this.get("cells"), function(cell){
            if (cell) cell.destroyParts();
        });
        this._updateForMode();
    },

    _updateForMode: function(){
        var cellMode = dmaGlobals.appState.get("cellMode");
        var partType =  this.get("partType");
        var scale = this.get("scale");
        this._iterCells(this.get("cells"), function(cell){
            if (cell) cell.draw(scale, cellMode, partType);
        });
        dmaGlobals.three.render();
    },

    _updateCellSeparation: function(){
        var cellSep = this.get("cellSeparation");
        this.get("basePlane").updateXYSeparation(cellSep.xy);

        var scale = this.get("scale");
        var cellMode = dmaGlobals.appState.get("cellMode");
        var partType = this.get("partType");
        this._iterCells(this.get("cells"), function(cell){
            if (cell) cell.updateForScale(scale, cellMode, partType);
        });
        dmaGlobals.three.render();
    },

    _scaleDidChange: function(){
        var scale = this.get("scale");
        this.get("basePlane").updateScale(scale);
        this.get("highlighter").updateScale(scale);

        var cellMode = dmaGlobals.appState.get("cellMode");
        var partType = this.get("partType");
        this._iterCells(this.get("cells"), function(cell){
            if (cell) cell.updateForScale(scale, cellMode, partType);
        });

        dmaGlobals.three.render();
    },

    previewScaleChange: function(scale){
        this.get("basePlane").updateScale(scale);
    },

    //hide show cells during stock simulation
    hideCells: function(){
        this._iterCells(this.get("cells"), function(cell){
            if (cell) cell.hide();
        });
        dmaGlobals.three.render();
    },

    showCells: function(){
        this._iterCells(this.get("cells"), function(cell){
            if (cell) cell.draw();
        });
        dmaGlobals.three.render();
    },

    showCellAtIndex: function(index){
        var latticeIndex = this._subtract(index, this.get("cellsMin"));
        var cell = this.get("cells")[latticeIndex.x][latticeIndex.y][latticeIndex.z];
        if (cell) cell.draw();
        else console.warn("placing a cell that does not exist");
    },

    ////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////CONNECTION TYPE//////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////

    _updateLatticeType: function(arg1, arg2, arg3, loadingFromFile){//do not clear cells if loading from file (cells array contains important metadata)
        if (typeof loadingFromFile == "undefined") loadingFromFile = false;
        var cellType = this.get("cellType");
        var connectionType = this.get("connectionType");
        if (this._undo) this._undo();
        if (this.get("basePlane")) this.get("basePlane").destroy();
        if (this.get("highlighter")) this.get("highlighter").destroy();
        if (cellType == "octa"){
            if (connectionType == "face"){
                _.extend(this, this.OctaFaceLattice);
            } else if (connectionType == "freeformFace"){
                if (!loadingFromFile) this.clearCells();
                _.extend(this, this.OctaFreeFormFaceLattice);
            } else if (connectionType == "edge"){
                _.extend(this, this.OctaFaceLattice);
                _.extend(this, this.OctaEdgeLattice);
            } else if (connectionType == "edgeRot"){
                _.extend(this, this.OctaRotEdgeLattice);
            } else if (connectionType == "vertex"){
                _.extend(this, this.OctaVertexLattice);
            }
        } else if (cellType == "tetra"){
            _.extend(this, this.CubeLattice);
        } else if (cellType == "cube"){
            _.extend(this, this.CubeLattice);
        } else if (cellType == "truncatedCube"){
            _.extend(this, this.TruncatedCubeLattice);
        } else if (cellType == "kelvin"){
            _.extend(this, this.KelvinLattice);
        }
        this._initLatticeType();

        //todo refactor this eventually
        var self = this;
        var scale = this.get("scale");
        var cells = this.get("cells");
        this._loopCells(cells, function(cell, x, y, z){
            if (!cell) return;

            var index = _.clone(cell.indices);
            //var  parts = null;
            //if (loadingFromFile) parts = _.clone(cell.parts);
            if (cell.parentOrientation) var parentOrientation = new THREE.Quaternion(cell.parentOrientation._x, cell.parentOrientation._y, cell.parentOrientation._z, cell.parentOrientation._w);
            if (cell.parentPosition) var parentPos = cell.parentPosition;
            if (cell.direction) var direction = new THREE.Vector3(cell.direction.x, cell.direction.y, cell.direction.z);
            if (cell.parentType) var parentType = cell.parentType;
            if (cell.type) var type = cell.type;

            if (cell.destroy) cell.destroy();
            var newCell = self.makeCellForLatticeType(index, scale, parentPos, parentOrientation, direction, parentType, type);

            //if (parts) {
            //    //todo make this better
            //    newCell.parts = newCell._initParts();
            //    for (var i=0;i<newCell.parts.length;i++){
            //        if (!parts[i]) {
            //            newCell.parts[i].destroy();
            //            newCell.parts[i] = null;
            //        }
            //    }
            //}
            cells[x][y][z] = newCell;
        });
        dmaGlobals.three.render();
    },

    ////////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////UTILS///////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////

    _iterCells: function(cells, callback){
        _.each(cells, function(cellLayer){
            _.each(cellLayer, function(cellColumn){
                _.each(cellColumn, function(cell){
                    callback(cell, cellColumn, cellLayer);
                });
            });

        });
    },

    _loopCells: function(cells, callback){
        for (var x=0;x<cells.length;x++){
            for (var y=0;y<cells[0].length;y++){
                for (var z=0;z<cells[0][0].length;z++){
                    callback(cells[x][y][z], x, y, z);
                }
            }
        }
    },

    rasterCells: function(order, callback, var1, var2, var3, cells){//used for CAM raster x/y/z in any order permutation
        //order is of form 'XYZ'
        var firstLetter = order.charAt(0);
        order = order.substr(1);
        var isNeg = false;
        if (firstLetter == "-") {
            isNeg = true;
            firstLetter = order.charAt(0);
            order = order.substr(1);
        }
        if (!cells) cells = this.get("cells");//grab cells once at beginning and hold onto it in case changes are made while looping
        var newVarOrder;
        var newVarDim;
        if (firstLetter == 'X'){
            newVarOrder = 0;
            newVarDim = cells.length;
        } else if (firstLetter == 'Y'){
            newVarOrder = 1;
            newVarDim = cells[0].length;
        } else if (firstLetter == 'Z'){
            newVarOrder = 2;
            newVarDim = cells[0][0].length;
        } else if (firstLetter == ""){
            for (var i=this._getRasterLoopInit(var1);this._getRasterLoopCondition(i,var1);i+=this._getRasterLoopIterator(var1)){
                for (var j=this._getRasterLoopInit(var2);this._getRasterLoopCondition(j,var2);j+=this._getRasterLoopIterator(var2)){
                    for (var k=this._getRasterLoopInit(var3);this._getRasterLoopCondition(k,var3);k+=this._getRasterLoopIterator(var3)){
                        if (var1.order == 0){
                            if (var2.order == 1) callback(cells[i][j][k], i, j, k);
                            else if (var2.order == 2) callback(cells[i][k][j], i, k, j);
                        } else if (var1.order == 1){
                            if (var2.order == 0) callback(cells[j][i][k], j, i, k);
                            else if (var2.order == 2) callback(cells[k][i][j], k, i, j);
                        } else {
                            if (var2.order == 0) callback(cells[j][k][i], j, k, i);
                            else if (var2.order == 1) {
                                callback(cells[k][j][i], k, j, i);
                            }
                        }

                    }
                }
            }
            return;
        }
        if (var3 == null) var3 = {order: newVarOrder, dim: newVarDim, neg:isNeg};
        else if (var2  == null) var2 = {order: newVarOrder, dim: newVarDim, neg:isNeg};
        else var1 = {order: newVarOrder, dim: newVarDim, neg:isNeg};
        this.rasterCells(order, callback, var1, var2, var3, cells);
    },

    _getRasterLoopInit: function(variable){
        if (variable.neg) return variable.dim-1;
        return 0;
    },

    _getRasterLoopCondition: function(iter, variable){
        if (variable.neg) return iter>=0;
        return iter<variable.dim;
    },

    _getRasterLoopIterator: function(variable){
        if (variable.neg) return -1;
        return 1;
    }

});