'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.parseData = parseData;
exports.updateScene = updateScene;

var _three = require('three');

var Three = _interopRequireWildcard(_three);

var _gridCreator = require('./grid-creator');

var _gridCreator2 = _interopRequireDefault(_gridCreator);

var _threeMemoryCleaner = require('./three-memory-cleaner');

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

function parseData(sceneData, actions, catalog) {

  var planData = {};

  planData.sceneGraph = {
    unit: sceneData.unit,
    layers: {},
    busyResources: { layers: {} },
    width: sceneData.width,
    height: sceneData.height,
    LODs: {}
  };

  planData.plan = new Three.Object3D();
  planData.plan.name = 'plan';

  // Add a grid to the plan
  planData.grid = (0, _gridCreator2.default)(sceneData);
  planData.grid.name = 'grid';

  planData.boundingBox = new Three.Box3().setFromObject(planData.grid);
  planData.boundingBox.name = 'boundingBox';

  var promises = [];

  sceneData.layers.forEach(function (layer) {

    if (layer.id === sceneData.selectedLayer || layer.visible) {
      promises = promises.concat(createLayerObjects(layer, planData, sceneData, actions, catalog));
    }
  });

  Promise.all(promises).then(function (value) {
    return updateBoundingBox(planData);
  });

  return planData;
}

function createLayerObjects(layer, planData, sceneData, actions, catalog) {

  var promises = [];

  planData.sceneGraph.layers[layer.id] = {
    id: layer.id,
    lines: {},
    holes: {},
    areas: {},
    items: {},
    visible: layer.visible,
    altitude: layer.altitude,
    visibleItems: layer.visibleItems
  };

  planData.sceneGraph.busyResources.layers[layer.id] = {
    id: layer.id,
    lines: {},
    holes: {},
    areas: {},
    items: {}
  };

  // Import lines
  layer.lines.forEach(function (line) {
    if (!layer.visibleItems[line.id]) {
      promises.push(addLine(sceneData, planData, layer, line.id, catalog, actions.linesActions));
      line.holes.forEach(function (holeID) {
        promises.push(addHole(sceneData, planData, layer, holeID, catalog, actions.holesActions));
      });
    }
  });

  // Import areas
  layer.areas.forEach(function (area) {
    if (!layer.visibleItems[area.id]) promises.push(addArea(sceneData, planData, layer, area.id, catalog, actions.areaActions));
  });

  // Import items
  layer.items.forEach(function (item) {
    if (!layer.visibleItems[item.id]) promises.push(addItem(sceneData, planData, layer, item.id, catalog, actions.itemsActions));
  });

  return promises;
}

function updateScene(planData, sceneData, oldSceneData, diffArray, actions, catalog) {

  var splitted = diffArray.map(function (el) {
    return { op: el.op, path: el.path.split('/'), value: el.value };
  });
  var filteredDiffs = filterDiffs(splitted, sceneData, oldSceneData);

  //***testing additional filter***
  filteredDiffs = filteredDiffs.filter(function (_ref) {
    var path = _ref.path;
    return path[3] !== 'selected';
  });
  filteredDiffs = filteredDiffs.filter(function (_ref2) {
    var path = _ref2.path;
    return path[1] !== 'groups';
  });
  //*******************************


  filteredDiffs.forEach(function (_ref3) {
    var op = _ref3.op,
        path = _ref3.path,
        value = _ref3.value;

    /* First of all I need to find the object I need to update */
    if (path[1] === 'layers') {

      var layer = sceneData.getIn(['layers', path[2]]);

      if (path.length === 3 && op === 'remove') {
        removeLayer(path[2], planData);
      } else if (path.length > 3) {
        switch (op) {
          case 'replace':
            replaceObject(path, layer, planData, actions, sceneData, oldSceneData, catalog);
            break;
          case 'add':
            addObject(path, layer, planData, actions, sceneData, oldSceneData, catalog);
            break;
          case 'remove':
            removeObject(path, layer, planData, actions, sceneData, oldSceneData, catalog);
            break;
        }
      }
    } else if (path[1] === 'selectedLayer') {
      var layerSelectedID = value;
      var layerSelected = sceneData.getIn(['layers', layerSelectedID]);
      // First of all I check if the new selected layer is not visible
      if (!layerSelected.visible) {
        // I need to create the objects for this layer
        var promises = createLayerObjects(layerSelected, planData, sceneData, actions, catalog);
        Promise.all(promises).then(function () {
          return updateBoundingBox(planData);
        });
      }

      var layerGraph = planData.sceneGraph.layers[oldSceneData.selectedLayer];

      if (layerGraph) {
        if (!layerGraph.visible) {
          // I need to remove the objects for this layer
          for (var lineID in layerGraph.lines) {
            removeLine(planData, layerGraph.id, lineID);
          }for (var areaID in layerGraph.areas) {
            removeArea(planData, layerGraph.id, areaID);
          }for (var itemID in layerGraph.items) {
            removeItem(planData, layerGraph.id, itemID);
          }for (var holeID in layerGraph.holes) {
            removeHole(planData, layerGraph.id, holeID);
          }
        }
      }
    }
  });
  return planData;
}

function replaceObject(modifiedPath, layer, planData, actions, sceneData, oldSceneData, catalog) {

  var promises = [];

  switch (modifiedPath[3]) {
    case 'vertices':
      if (modifiedPath[5] !== 'selected') {
        var vertex = layer.getIn(['vertices', modifiedPath[4]]);

        if (modifiedPath[5] === 'x' || modifiedPath[5] === 'y') {
          vertex.lines.forEach(function (lineID) {
            var lineHoles = oldSceneData.getIn(['layers', layer.id, 'lines', lineID, 'holes']);
            if (lineHoles) lineHoles.forEach(function (holeID) {
              replaceObject([0, 0, 0, 'holes', holeID, 'selected'], layer, planData, actions, sceneData, oldSceneData, catalog);
            });
            return replaceObject([0, 0, 0, 'lines', lineID], layer, planData, actions, sceneData, oldSceneData, catalog);
          });
          vertex.areas.forEach(function (areaID) {
            return replaceObject([0, 0, 0, 'areas', areaID], layer, planData, actions, sceneData, oldSceneData, catalog);
          });
        }

        if (modifiedPath[5] === 'areas') {
          var areaID = vertex.getIn(['areas', ~~modifiedPath[6]]);
          replaceObject([0, 0, 0, 'areas', areaID], layer, planData, actions, sceneData, oldSceneData, catalog);
        }
      }
      break;
    case 'holes':
      var newHoleData = layer.getIn(['holes', modifiedPath[4]]);

      if (catalog.getElement(newHoleData.type).updateRender3D) {
        promises.push(updateHole(sceneData, oldSceneData, planData, layer, modifiedPath[4], modifiedPath.slice(5), catalog, actions.holesActions, function () {
          return removeHole(planData, layer.id, newHoleData.id);
        }, function () {
          return addHole(sceneData, planData, layer, newHoleData.id, catalog, actions.holesActions);
        }));
      } else {
        var lineID = newHoleData.line;
        if (modifiedPath[5] === 'selected') {
          // I remove only the hole without removing the wall
          removeHole(planData, layer.id, newHoleData.id);
          promises.push(addHole(sceneData, planData, layer, newHoleData.id, catalog, actions.holesActions));
        } else {
          layer.getIn(['lines', lineID, 'holes']).forEach(function (holeID) {
            removeHole(planData, layer.id, holeID);
          });
          removeLine(planData, layer.id, lineID);
          promises.push(addLine(sceneData, planData, layer, lineID, catalog, actions.linesActions));
          layer.getIn(['lines', lineID, 'holes']).forEach(function (holeID) {
            promises.push(addHole(sceneData, planData, layer, holeID, catalog, actions.holesActions));
          });
        }
      }
      break;
    case 'lines':
      var line = layer.getIn(['lines', modifiedPath[4]]);

      if (catalog.getElement(line.type).updateRender3D) {
        promises.push(updateLine(sceneData, oldSceneData, planData, layer, modifiedPath[4], modifiedPath.slice(5), catalog, actions.linesActions, function () {
          return removeLine(planData, layer.id, modifiedPath[4]);
        }, function () {
          return addLine(sceneData, planData, layer, modifiedPath[4], catalog, actions.linesActions);
        }));
      } else {
        removeLine(planData, layer.id, modifiedPath[4]);
        promises.push(addLine(sceneData, planData, layer, modifiedPath[4], catalog, actions.linesActions));
      }
      break;
    case 'areas':
      var area = layer.getIn(['areas', modifiedPath[4]]);

      if (catalog.getElement(area.type).updateRender3D) {
        promises.push(updateArea(sceneData, oldSceneData, planData, layer, modifiedPath[4], modifiedPath.slice(5), catalog, actions.areaActions, function () {
          return removeArea(planData, layer.id, modifiedPath[4]);
        }, function () {
          return addArea(sceneData, planData, layer, modifiedPath[4], catalog, actions.areaActions);
        }));
      } else {
        if (planData.sceneGraph.layers[layer.id].areas[modifiedPath[4]]) {
          removeArea(planData, layer.id, modifiedPath[4]);
        }
        promises.push(addArea(sceneData, planData, layer, modifiedPath[4], catalog, actions.areaActions));
      }
      break;
    case 'items':

      var item = layer.getIn(['items', modifiedPath[4]]);
      if (catalog.getElement(item.type).updateRender3D) {

        promises.push(updateItem(sceneData, oldSceneData, planData, layer, modifiedPath[4], modifiedPath.slice(5), catalog, actions.itemsActions, function () {
          return removeItem(planData, layer.id, modifiedPath[4]);
        }, function () {
          return addItem(sceneData, planData, layer, modifiedPath[4], catalog, actions.itemsActions);
        }));
      } else {
        promises.push(removeItem(planData, layer.id, modifiedPath[4]));
        promises.push(addItem(sceneData, planData, layer, modifiedPath[4], catalog, actions.itemsActions));
      }
      break;

    case 'visible':
      if (!layer.visible) {
        var _layerGraph = planData.sceneGraph.layers[layer.id];

        for (var _lineID in _layerGraph.lines) {
          removeLine(planData, layer.id, _lineID);
        }for (var _areaID in _layerGraph.areas) {
          removeArea(planData, layer.id, _areaID);
        }for (var itemID in _layerGraph.items) {
          removeItem(planData, layer.id, itemID);
        }for (var holeID in _layerGraph.holes) {
          removeHole(planData, layer.id, holeID);
        }
      } else {
        promises = promises.concat(createLayerObjects(layer, planData, sceneData, actions, catalog));
      }

      break;

    case 'opacity':
    case 'altitude':
      var layerGraph = planData.sceneGraph.layers[layer.id];
      for (var _lineID2 in layerGraph.lines) {
        removeLine(planData, layer.id, _lineID2);
      }for (var _areaID2 in layerGraph.areas) {
        removeArea(planData, layer.id, _areaID2);
      }for (var _itemID in layerGraph.items) {
        removeItem(planData, layer.id, _itemID);
      }for (var _holeID in layerGraph.holes) {
        removeHole(planData, layer.id, _holeID);
      }promises = promises.concat(createLayerObjects(layer, planData, sceneData, actions, catalog));

  }
  Promise.all(promises).then(function (values) {
    return updateBoundingBox(planData);
  });
}

function removeObject(modifiedPath, layer, planData, actions, sceneData, oldSceneData, catalog) {

  var promises = [];
  switch (modifiedPath[3]) {
    case 'lines':
      // Here I remove the line with all its holes
      var lineID = modifiedPath[4];
      oldSceneData.getIn(['layers', layer.id, 'lines', lineID, 'holes']).forEach(function (holeID) {
        removeHole(planData, layer.id, holeID);
      });
      removeLine(planData, layer.id, lineID);
      if (modifiedPath.length > 5) {
        // I removed an hole, so I should add the new line
        promises.push(addLine(sceneData, planData, layer, lineID, catalog, actions.linesActions));
        layer.getIn(['lines', lineID, 'holes']).forEach(function (holeID) {
          promises.push(addHole(sceneData, planData, layer, holeID, catalog, actions.holesActions));
        });
      }
      break;
    case 'areas':
      if (modifiedPath.length === 5) {
        // I am removing an entire area
        removeArea(planData, layer.id, modifiedPath[4]);
      }
      break;
    case 'items':
      if (modifiedPath.length === 5) {
        // I am removing an item
        removeItem(planData, layer.id, modifiedPath[4]);
      }
      break;
  }

  Promise.all(promises).then(function (values) {
    return updateBoundingBox(planData);
  });
}

function removeLayer(layerId, planData) {
  var layerGraph = planData.sceneGraph.layers[layerId];

  for (var lineID in layerGraph.lines) {
    removeLine(planData, layerId, lineID);
  }for (var areaID in layerGraph.areas) {
    removeArea(planData, layerId, areaID);
  }for (var itemID in layerGraph.items) {
    removeItem(planData, layerId, itemID);
  }for (var holeID in layerGraph.holes) {
    removeHole(planData, layerId, holeID);
  }delete planData.sceneGraph.layers[layerId];
}

function removeHole(planData, layerId, holeID) {

  if (planData.sceneGraph.busyResources.layers[layerId].holes[holeID]) {
    setTimeout(function () {
      return removeHole(planData, layerId, holeID);
    }, 100);
    return;
  }

  planData.sceneGraph.busyResources.layers[layerId].holes[holeID] = true;

  var hole3D = planData.sceneGraph.layers[layerId].holes[holeID];

  if (hole3D) {
    planData.plan.remove(hole3D);
    (0, _threeMemoryCleaner.disposeObject)(hole3D);
    delete planData.sceneGraph.layers[layerId].holes[holeID];
    delete planData.sceneGraph.LODs[holeID];
    hole3D = null;
    updateBoundingBox(planData);
  }

  planData.sceneGraph.busyResources.layers[layerId].holes[holeID] = false;
}

function removeLine(planData, layerId, lineID) {

  if (planData.sceneGraph.busyResources.layers[layerId].lines[lineID]) {
    setTimeout(function () {
      return removeLine(planData, layerId, lineID);
    }, 100);
    return;
  }

  planData.sceneGraph.busyResources.layers[layerId].lines[lineID] = true;

  var line3D = planData.sceneGraph.layers[layerId].lines[lineID];

  if (line3D) {
    planData.plan.remove(line3D);
    (0, _threeMemoryCleaner.disposeObject)(line3D);
    delete planData.sceneGraph.layers[layerId].lines[lineID];
    delete planData.sceneGraph.LODs[lineID];
    line3D = null;
    updateBoundingBox(planData);
  }

  planData.sceneGraph.busyResources.layers[layerId].lines[lineID] = false;
}

function removeArea(planData, layerId, areaID) {

  if (planData.sceneGraph.busyResources.layers[layerId].areas[areaID]) {
    setTimeout(function () {
      return removeArea(planData, layerId, areaID);
    }, 100);
    return;
  }

  planData.sceneGraph.busyResources.layers[layerId].areas[areaID] = true;

  var area3D = planData.sceneGraph.layers[layerId].areas[areaID];

  if (area3D) {
    planData.plan.remove(area3D);
    (0, _threeMemoryCleaner.disposeObject)(area3D);
    delete planData.sceneGraph.layers[layerId].areas[areaID];
    delete planData.sceneGraph.LODs[areaID];
    area3D = null;
    updateBoundingBox(planData);
  }

  planData.sceneGraph.busyResources.layers[layerId].areas[areaID] = false;
}

function removeItem(planData, layerId, itemID) {

  if (planData.sceneGraph.busyResources.layers[layerId].items[itemID]) {
    setTimeout(function () {
      return removeItem(planData, layerId, itemID);
    }, 100);
    return;
  }

  planData.sceneGraph.busyResources.layers[layerId].items[itemID] = true;

  var item3D = planData.sceneGraph.layers[layerId].items[itemID];

  if (item3D) {
    planData.plan.remove(item3D);

    (0, _threeMemoryCleaner.disposeObject)(item3D);
    delete planData.sceneGraph.layers[layerId].items[itemID];
    delete planData.sceneGraph.LODs[itemID];
    item3D = null;
    updateBoundingBox(planData);
  }

  planData.sceneGraph.busyResources.layers[layerId].items[itemID] = false;
}

//TODO generate an area's replace if vertex has been changed
function addObject(modifiedPath, layer, planData, actions, sceneData, oldSceneData, catalog) {
  if (modifiedPath.length === 5) {
    var addPromise = null,
        addAction = null;

    switch (modifiedPath[3]) {
      case 'lines':
        addPromise = addLine;addAction = actions.linesActions;break;
      case 'areas':
        addPromise = addArea;addAction = actions.areaActions;break;
      case 'items':
        addPromise = addItem;addAction = actions.itemsActions;break;
      case 'holes':
        addPromise = addHole;addAction = actions.holesActions;break;
    }

    if (addPromise) addPromise(sceneData, planData, layer, modifiedPath[4], catalog, addAction).then(function () {
      return updateBoundingBox(planData);
    });
  }
}

function addHole(sceneData, planData, layer, holeID, catalog, holesActions) {
  var holeData = layer.getIn(['holes', holeID]);

  // Create the hole object
  return catalog.getElement(holeData.type).render3D(holeData, layer, sceneData).then(function (object) {

    if (object instanceof Three.LOD) {
      planData.sceneGraph.LODs[holeID] = object;
    }

    var pivot = new Three.Object3D();
    pivot.name = 'pivot';
    pivot.add(object);

    var line = layer.getIn(['lines', holeData.line]);

    // First of all I need to find the vertices of this line
    var vertex0 = layer.vertices.get(line.vertices.get(0));
    var vertex1 = layer.vertices.get(line.vertices.get(1));
    var offset = holeData.offset;

    if (vertex0.x > vertex1.x) {
      var tmp = vertex0;
      vertex0 = vertex1;
      vertex1 = tmp;
      offset = 1 - offset;
    }

    var distance = Math.sqrt(Math.pow(vertex0.x - vertex1.x, 2) + Math.pow(vertex0.y - vertex1.y, 2));
    var alpha = Math.asin((vertex1.y - vertex0.y) / distance);

    var boundingBox = new Three.Box3().setFromObject(pivot);
    var center = [(boundingBox.max.x - boundingBox.min.x) / 2 + boundingBox.min.x, (boundingBox.max.y - boundingBox.min.y) / 2 + boundingBox.min.y, (boundingBox.max.z - boundingBox.min.z) / 2 + boundingBox.min.z];

    var holeAltitude = holeData.properties.getIn(['altitude', 'length']);
    var holeHeight = holeData.properties.getIn(['height', 'length']);

    pivot.rotation.y = alpha;
    pivot.position.x = vertex0.x + distance * offset * Math.cos(alpha) - center[2] * Math.sin(alpha);
    pivot.position.y = holeAltitude + holeHeight / 2 - center[1] + layer.altitude;
    pivot.position.z = -vertex0.y - distance * offset * Math.sin(alpha) - center[2] * Math.cos(alpha);

    planData.plan.add(pivot);

    planData.sceneGraph.layers[layer.id].holes[holeData.id] = pivot;

    applyInteract(pivot, function () {
      return holesActions.selectHole(layer.id, holeData.id);
    });

    var opacity = layer.opacity;
    if (holeData.selected) {
      opacity = 1;
    }
    applyOpacity(pivot, opacity);
  });
}

function updateHole(sceneData, oldSceneData, planData, layer, holeID, differences, catalog, holesActions, selfDestroy, selfBuild) {
  var hole = layer.getIn(['holes', holeID]);
  var oldHole = oldSceneData.getIn(['layers', layer.id, 'holes', holeID]);
  var mesh = planData.sceneGraph.layers[layer.id].holes[holeID];

  if (!mesh) return null;

  return catalog.getElement(hole.type).updateRender3D(hole, layer, sceneData, mesh, oldHole, differences, selfDestroy, selfBuild);
}

function addLine(sceneData, planData, layer, lineID, catalog, linesActions) {

  if (planData.sceneGraph.busyResources.layers[layer.id].lines[lineID]) {
    setTimeout(function () {
      return addLine(sceneData, planData, layer, lineID, catalog, linesActions);
    }, 100);
    return;
  }

  planData.sceneGraph.busyResources.layers[layer.id].lines[lineID] = true;

  var line = layer.getIn(['lines', lineID]);

  // First of all I need to find the vertices of this line
  var vertex0 = layer.vertices.get(line.vertices.get(0));
  var vertex1 = layer.vertices.get(line.vertices.get(1));

  if (vertex0.x > vertex1.x) {
    var tmp = vertex0;
    vertex0 = vertex1;
    vertex1 = tmp;
  }

  return catalog.getElement(line.type).render3D(line, layer, sceneData).then(function (line3D) {

    if (line3D instanceof Three.LOD) {
      planData.sceneGraph.LODs[line.id] = line3D;
    }

    var pivot = new Three.Object3D();
    pivot.name = 'pivot';
    pivot.add(line3D);

    pivot.position.x = vertex0.x;
    pivot.position.y = layer.altitude;
    pivot.position.z = -vertex0.y;

    planData.plan.add(pivot);
    planData.sceneGraph.layers[layer.id].lines[lineID] = pivot;

    applyInteract(pivot, function () {
      return linesActions.selectLine(layer.id, line.id);
    });

    var opacity = layer.opacity;
    if (line.selected) {
      opacity = 1;
    }
    applyOpacity(pivot, opacity);
    planData.sceneGraph.busyResources.layers[layer.id].lines[lineID] = false;
  });
}

function updateLine(sceneData, oldSceneData, planData, layer, lineID, differences, catalog, linesActions, selfDestroy, selfBuild) {
  var line = layer.getIn(['lines', lineID]);
  var oldLine = oldSceneData.getIn(['layers', layer.id, 'lines', lineID]);
  var mesh = planData.sceneGraph.layers[layer.id].lines[lineID];
  if (!mesh) return null;
  return catalog.getElement(line.type).updateRender3D(line, layer, sceneData, mesh, oldLine, differences, selfDestroy, selfBuild);
}

function addArea(sceneData, planData, layer, areaID, catalog, areaActions) {

  if (planData.sceneGraph.busyResources.layers[layer.id].areas[areaID]) {
    setTimeout(function () {
      return addArea(sceneData, planData, layer, areaID, catalog, areaActions);
    }, 100);
    return;
  }

  planData.sceneGraph.busyResources.layers[layer.id].areas[areaID] = true;

  var area = layer.getIn(['areas', areaID]);
  var interactFunction = function interactFunction() {
    return areaActions.selectArea(layer.id, areaID);
  };

  return catalog.getElement(area.type).render3D(area, layer, sceneData).then(function (area3D) {

    if (area3D instanceof Three.LOD) {
      planData.sceneGraph.LODs[areaID] = area3D;
    }

    var pivot = new Three.Object3D();
    pivot.name = 'pivot';
    pivot.add(area3D);
    pivot.position.y = layer.altitude;
    planData.plan.add(pivot);
    planData.sceneGraph.layers[layer.id].areas[areaID] = pivot;

    applyInteract(pivot, interactFunction);

    var opacity = layer.opacity;
    if (area.selected) {
      opacity = 1;
    }

    applyOpacity(pivot, opacity);
    planData.sceneGraph.busyResources.layers[layer.id].areas[areaID] = false;
  });
}

function updateArea(sceneData, oldSceneData, planData, layer, areaID, differences, catalog, areaActions, selfDestroy, selfBuild) {
  var area = layer.getIn(['areas', areaID]);
  var oldArea = oldSceneData.getIn(['layers', layer.id, 'areas', areaID]);
  var mesh = planData.sceneGraph.layers[layer.id].areas[areaID];
  if (!mesh) return null;
  return catalog.getElement(area.type).updateRender3D(area, layer, sceneData, mesh, oldArea, differences, selfDestroy, selfBuild);
}

function addItem(sceneData, planData, layer, itemID, catalog, itemsActions) {

  var item = layer.getIn(['items', itemID]);

  return catalog.getElement(item.type).render3D(item, layer, sceneData).then(function (item3D) {

    if (item3D instanceof Three.LOD) {
      planData.sceneGraph.LODs[itemID] = item3D;
    }

    var pivot = new Three.Object3D();
    pivot.name = 'pivot';
    pivot.add(item3D);

    pivot.rotation.y = item.rotation * Math.PI / 180;
    pivot.position.x = item.x;
    pivot.position.y = layer.altitude;
    pivot.position.z = -item.y;

    applyInteract(item3D, function (event) {
      if (event == 'click') itemsActions.selectItem(layer.id, item.id);else if (event == 'hover') {
        itemsActions.hoverItem(layer.id, item.id);
      }
    });

    var opacity = layer.opacity;
    if (item.selected) {
      opacity = 1;
    }
    applyOpacity(pivot, opacity);
    planData.plan.add(pivot);
    if (planData.plan.children.length > 1000) {
      planData.plan.remove(planData.plan.children[0]);
    }
    planData.sceneGraph.layers[layer.id].items[item.id] = pivot;
  });
}

function updateItem(sceneData, oldSceneData, planData, layer, itemID, differences, catalog, itemsActions, selfDestroy, selfBuild) {

  var item = layer.getIn(['items', itemID]);
  var oldItem = oldSceneData.getIn(['layers', layer.id, 'items', itemID]);
  var mesh = planData.sceneGraph.layers[layer.id].items[itemID];
  if (!mesh) return null;
  return catalog.getElement(item.type).updateRender3D(item, layer, sceneData, mesh, oldItem, differences, selfDestroy, selfBuild);
}

// Apply interact function to children of an Object3D
function applyInteract(object, interactFunction) {
  object.traverse(function (child) {
    if (child instanceof Three.Mesh) {
      child.interact = interactFunction;
    }
  });
}

// Apply opacity to children of an Object3D
function applyOpacity(object, opacity) {
  object.traverse(function (child) {

    if (child instanceof Three.Mesh) {
      if (child.material instanceof Three.MultiMaterial) {
        child.material.materials.forEach(function (materialChild) {
          materialChild.transparent = true;
          if (materialChild.maxOpacity) {
            materialChild.opacity = Math.min(materialChild.maxOpacity, opacity);
          } else if (materialChild.opacity && materialChild.opacity > opacity) {
            materialChild.maxOpacity = materialChild.opacity;
            materialChild.opacity = opacity;
          }
        });
      } else if (child.material instanceof Array) {
        child.material.forEach(function (material) {
          material.transparent = true;
          if (material.maxOpacity) {
            material.opacity = Math.min(material.maxOpacity, opacity);
          } else if (material.opacity && material.opacity > opacity) {
            material.maxOpacity = material.opacity;
            material.opacity = opacity;
          }
        });
      } else {
        child.material.transparent = true;
        if (child.material.maxOpacity) {
          child.material.opacity = Math.min(child.material.maxOpacity, opacity);
        } else if (child.material.opacity && child.material.opacity > opacity) {
          child.material.maxOpacity = child.material.opacity;
          child.material.opacity = opacity;
        }
      }
    }
  });
}

function updateBoundingBox(planData) {
  var newBoundingBox = new Three.Box3().setFromObject(planData.plan);

  if (isFinite(newBoundingBox.max.x) && isFinite(newBoundingBox.min.x) && isFinite(newBoundingBox.max.y) && isFinite(newBoundingBox.min.y) && isFinite(newBoundingBox.max.z) && isFinite(newBoundingBox.min.z)) {

    var newCenter = new Three.Vector3((newBoundingBox.max.x - newBoundingBox.min.x) / 2 + newBoundingBox.min.x, (newBoundingBox.max.y - newBoundingBox.min.y) / 2 + newBoundingBox.min.y, (newBoundingBox.max.z - newBoundingBox.min.z) / 2 + newBoundingBox.min.z);

    planData.plan.position.sub(newCenter);
    planData.grid.position.sub(newCenter);

    newBoundingBox.min.sub(newCenter);
    newBoundingBox.max.sub(newCenter);

    planData.boundingBox = newBoundingBox;
  }
}

/**
 * Filter the array of diffs
 * @param diffArray
 * @param sceneData
 * @param oldSceneData
 * @returns {Array}
 */
function filterDiffs(diffArray, sceneData, oldSceneData) {
  return minimizeRemoveDiffsWhenSwitchingLayers(minimizeChangePropertiesAfterSelectionsDiffs(minimizeChangePropertiesDiffs(diffArray, sceneData, oldSceneData), sceneData, oldSceneData), sceneData, oldSceneData);
}

/**
 * Reduces the number of remove diffs when switching an hidden layer
 * @param diffArray the array of the diffs
 * @param sceneData
 * @param oldSceneData
 * @returns {Array}
 */
function minimizeRemoveDiffsWhenSwitchingLayers(diffArray, sceneData, oldSceneData) {

  var foundDiff = void 0;
  var i = void 0;
  for (i = 0; i < diffArray.length && !foundDiff; i++) {
    if (diffArray[i].path[1] === 'selectedLayer') {
      foundDiff = diffArray[i];
    }
  }

  if (foundDiff) {
    if (!sceneData.getIn(['layers', oldSceneData.selectedLayer, 'visible'])) {
      return diffArray.filter(function (_ref4) {
        var op = _ref4.op,
            path = _ref4.path;


        return !(path[path.length - 1] === 'selected' && path[1] === 'layers' && path[2] === oldSceneData.selectedLayer) && !(op === 'remove' && path.indexOf(oldSceneData.selectedLayer) !== -1);
      });
    }
  }

  return diffArray;
}

/**
 * Reduces the number of change properties diffs for selected elements
 * @param diffArray the array of the diffs
 * @param sceneData
 * @param oldSceneData
 * @returns {Array}
 */
function minimizeChangePropertiesAfterSelectionsDiffs(diffArray, sceneData, oldSceneData) {
  var idsFound = {};
  diffArray.forEach(function (_ref5) {
    var path = _ref5.path;

    if (path[5] === 'selected') {
      idsFound[path[4]] = path[4];
    }
  });

  return diffArray.filter(function (_ref6) {
    var path = _ref6.path;

    if (path[5] === 'properties') {
      return idsFound[path[4]] ? false : true;
    }
    return true;
  });
}

/**
 * Reduces the number of change properties diffs
 * @param diffArray the array of the diffs
 * @param sceneData
 * @param oldSceneData
 * @returns {Array}
 */
function minimizeChangePropertiesDiffs(diffArray, sceneData, oldSceneData) {
  var idsFound = {};
  return diffArray.filter(function (_ref7) {
    var path = _ref7.path;

    if (path[5] === 'properties') {
      return idsFound[path[4]] ? false : idsFound[path[4]] = true;
    } else if (path[5] === 'misc') {
      // Remove misc changes
      return false;
    }
    return true;
  });
}