"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.$3dTilesCulling = $3dTilesCulling;
exports.$3dTilesSubdivisionControl = $3dTilesSubdivisionControl;
exports.computeNodeSSE = computeNodeSSE;
exports.init3dTilesLayer = init3dTilesLayer;
exports.pre3dTilesUpdate = pre3dTilesUpdate;
exports.process3dTilesNode = process3dTilesNode;
var THREE = _interopRequireWildcard(require("three"));
var _ObjectRemovalHelper = _interopRequireDefault(require("./ObjectRemovalHelper"));
var _C3DTilesEnums = require("../Core/3DTiles/C3DTilesEnums");
var _C3DTilesLayer = require("../Layer/C3DTilesLayer");
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
/** @module 3dTilesProcessing
*/

function requestNewTile(view, scheduler, geometryLayer, metadata, parent, redraw) {
  const command = {
    /* mandatory */
    view,
    requester: parent,
    layer: geometryLayer,
    priority: parent ? 1.0 / (parent.distance + 1) : 100,
    /* specific params */
    metadata,
    redraw
  };
  geometryLayer.dispatchEvent({
    type: _C3DTilesLayer.C3DTILES_LAYER_EVENTS.ON_TILE_REQUESTED,
    metadata
  });
  return scheduler.execute(command);
}
function getChildTiles(tile) {
  // only keep children that have the same layer and a valid tileId
  return tile.children.filter(n => n.layer == tile.layer && n.tileId);
}
function subdivideNode(context, layer, node, cullingTest) {
  if (node.additiveRefinement) {
    // Additive refinement can only fetch visible children.
    _subdivideNodeAdditive(context, layer, node, cullingTest);
  } else {
    // Substractive refinement on the other hand requires to replace
    // node with all of its children
    _subdivideNodeSubstractive(context, layer, node);
  }
}
const tmpMatrix = new THREE.Matrix4();
function _subdivideNodeAdditive(context, layer, node, cullingTest) {
  for (const child of layer.tileset.tiles[node.tileId].children) {
    // child being downloaded => skip
    if (child.promise || child.loaded) {
      continue;
    }

    // 'child' is only metadata (it's *not* a THREE.Object3D). 'cullingTest' needs
    // a matrixWorld, so we compute it: it's node's matrixWorld x child's transform
    let overrideMatrixWorld = node.matrixWorld;
    if (child.transform) {
      overrideMatrixWorld = tmpMatrix.multiplyMatrices(node.matrixWorld, child.transform);
    }
    const isVisible = cullingTest ? !cullingTest(layer, context.camera, child, overrideMatrixWorld) : true;

    // child is not visible => skip
    if (!isVisible) {
      continue;
    }
    child.promise = requestNewTile(context.view, context.scheduler, layer, child, node, true).then(tile => {
      node.add(tile);
      tile.updateMatrixWorld();
      layer.onTileContentLoaded(tile);
      context.view.notifyChange(child);
      child.loaded = true;
      delete child.promise;
    });
  }
}
function _subdivideNodeSubstractive(context, layer, node) {
  if (!node.pendingSubdivision && getChildTiles(node).length == 0) {
    const childrenTiles = layer.tileset.tiles[node.tileId].children;
    if (childrenTiles === undefined || childrenTiles.length === 0) {
      return;
    }
    node.pendingSubdivision = true;
    const promises = [];
    for (let i = 0; i < childrenTiles.length; i++) {
      promises.push(requestNewTile(context.view, context.scheduler, layer, childrenTiles[i], node, false).then(tile => {
        childrenTiles[i].loaded = true;
        node.add(tile);
        tile.updateMatrixWorld();
        // TODO: remove because cannot happen?
        if (node.additiveRefinement) {
          context.view.notifyChange(node);
        }
        layer.tileset.tiles[tile.tileId].loaded = true;
        layer.onTileContentLoaded(tile);
      }));
    }
    Promise.all(promises).then(() => {
      node.pendingSubdivision = false;
      context.view.notifyChange(node);
    });
  }
}

/**
 * Check if the node is visible in the camera.
 *
 * @param      {C3DTilesLayer} layer       node 3D tiles layer
 * @param      {Camera}   camera           camera
 * @param      {THREE.Object3D}   node             The 3d tile node to check.
 * @param      {THREE.Matrix4}   tileMatrixWorld  The node matrix world
 * @return     {boolean}  return true if the node is visible
 */
function $3dTilesCulling(layer, camera, node, tileMatrixWorld) {
  // For viewer Request Volume
  // https://github.com/AnalyticalGraphicsInc/3d-tiles-samples/tree/master/tilesets/TilesetWithRequestVolume
  if (node.viewerRequestVolume && node.viewerRequestVolume.viewerRequestVolumeCulling(camera, tileMatrixWorld)) {
    return true;
  }

  // For bounding volume
  return !!(node.boundingVolume && node.boundingVolume.boundingVolumeCulling(camera, tileMatrixWorld));
}

// Cleanup all 3dtiles|three.js starting from a given node n.
// n's children can be of 2 types:
//   - have a 'content' attribute -> it's a tileset and must
//     be cleaned with cleanup3dTileset()
//   - doesn't have 'content' -> it's a raw Object3D object,
//     and must be cleaned with _cleanupObject3D()
function cleanup3dTileset(layer, n) {
  let depth = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
  // If this layer is not using additive refinement, we can only
  // clean a tile if all its neighbours are cleaned as well because
  // a tile can only be in 2 states:
  //   - displayed and no children displayed
  //   - hidden and all of its children displayed
  // So here we implement a conservative measure: if T is cleanable
  // we actually only clean its children tiles.
  const canCleanCompletely = n.additiveRefinement || depth > 0;
  for (let i = 0; i < n.children.length; i++) {
    // skip non-tiles elements
    if (!n.children[i].content) {
      if (canCleanCompletely) {
        _ObjectRemovalHelper.default.removeChildrenAndCleanupRecursively(n.children[i].layer, n.children[i]);
      }
    } else {
      cleanup3dTileset(layer, n.children[i], depth + 1);
    }
  }
  if (canCleanCompletely) {
    if (n.dispose) {
      n.dispose();
    }
    delete n.content;
    layer.tileset.tiles[n.tileId].loaded = false;
    n.remove(...n.children);

    // and finally remove from parent
    if (depth == 0 && n.parent) {
      n.parent.remove(n);
    }
  } else {
    const tiles = getChildTiles(n);
    n.remove(...tiles);
  }
}

// this is a layer
function pre3dTilesUpdate() {
  if (!this.visible) {
    return [];
  }

  // Elements removed are added in the layer._cleanableTiles list.
  // Since we simply push in this array, the first item is always
  // the oldest one.
  const now = Date.now();
  if (this._cleanableTiles.length && now - this._cleanableTiles[0].cleanableSince > this.cleanupDelay) {
    // Make sure we don't clean root tile
    this.root.cleanableSince = undefined;
    let i = 0;
    for (; i < this._cleanableTiles.length; i++) {
      const elt = this._cleanableTiles[i];
      if (now - elt.cleanableSince > this.cleanupDelay) {
        cleanup3dTileset(this, elt);
      } else {
        // later entries are younger
        break;
      }
    }
    // remove deleted elements from _cleanableTiles
    this._cleanableTiles.splice(0, i);
  }
  return [this.root];
}
const boundingVolumeBox = new THREE.Box3();
const boundingVolumeSphere = new THREE.Sphere();
function computeNodeSSE(camera, node) {
  node.distance = 0;
  if (node.boundingVolume.initialVolumeType === _C3DTilesEnums.C3DTilesBoundingVolumeTypes.box) {
    boundingVolumeBox.copy(node.boundingVolume.volume);
    boundingVolumeBox.applyMatrix4(node.matrixWorld);
    node.distance = boundingVolumeBox.distanceToPoint(camera.camera3D.position);
  } else if (node.boundingVolume.initialVolumeType === _C3DTilesEnums.C3DTilesBoundingVolumeTypes.sphere || node.boundingVolume.initialVolumeType === _C3DTilesEnums.C3DTilesBoundingVolumeTypes.region) {
    boundingVolumeSphere.copy(node.boundingVolume.volume);
    boundingVolumeSphere.applyMatrix4(node.matrixWorld);
    // TODO: see https://github.com/iTowns/itowns/issues/800
    node.distance = Math.max(0.0, boundingVolumeSphere.distanceToPoint(camera.camera3D.position));
  } else {
    return Infinity;
  }
  if (node.distance === 0) {
    // This test is needed in case geometricError = distance = 0
    return Infinity;
  }
  return camera._preSSE * (node.geometricError / node.distance);
}
function init3dTilesLayer(view, scheduler, layer, rootTile) {
  return requestNewTile(view, scheduler, layer, rootTile, undefined, true).then(tile => {
    layer.object3d.add(tile);
    tile.updateMatrixWorld();
    layer.tileset.tiles[tile.tileId].loaded = true;
    layer.root = tile;
    layer.onTileContentLoaded(tile);
  });
}
function setDisplayed(node, display) {
  // The geometry of the tile is not in node, but in node.content
  // To change the display state, we change node.content.visible instead of
  // node.material.visible
  if (node.content) {
    node.content.visible = display;
  }
}
function markForDeletion(layer, elt) {
  if (!elt.cleanableSince) {
    elt.cleanableSince = Date.now();
    layer._cleanableTiles.push(elt);
  }
}

/**
 * This funcion builds the method to update 3d tiles node.
 *
 * The returned method checks the 3d tile visibility with `cullingTest` function.
 * It subdivises visible node if `subdivisionTest` return `true`.
 *
 * @param      {Function}  [cullingTest=$3dTilesCulling]                 The culling test method.
 * @param      {Function}  [subdivisionTest=$3dTilesSubdivisionControl]  The subdivision test method.
 * @return     {Function}    { description_of_the_return_value }
 */
function process3dTilesNode() {
  let cullingTest = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : $3dTilesCulling;
  let subdivisionTest = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : $3dTilesSubdivisionControl;
  return function (context, layer, node) {
    // early exit if parent's subdivision is in progress
    if (node.parent.pendingSubdivision && !node.parent.additiveRefinement) {
      node.visible = false;
      return undefined;
    }

    // do proper culling
    const isVisible = cullingTest ? !cullingTest(layer, context.camera, node, node.matrixWorld) : true;
    node.visible = isVisible;
    if (isVisible) {
      if (node.cleanableSince) {
        layer._cleanableTiles.splice(layer._cleanableTiles.indexOf(node), 1);
        node.cleanableSince = undefined;
      }
      let returnValue;
      if (node.pendingSubdivision || subdivisionTest(context, layer, node)) {
        subdivideNode(context, layer, node, cullingTest);
        // display iff children aren't ready
        setDisplayed(node, node.pendingSubdivision || node.additiveRefinement);
        returnValue = getChildTiles(node);
      } else {
        setDisplayed(node, true);
        for (const n of getChildTiles(node)) {
          n.visible = false;
          markForDeletion(layer, n);
        }
      }
      return returnValue;
    }
    markForDeletion(layer, node);
  };
}

/**
 *
 *
 * the method returns true if the `node` should be subivised.
 *
 * @param      {object}   context  The current context
 * @param      {Camera}   context.camera  The current camera
 * @param      {C3DTilesLayer}   layer  The 3d tile layer
 * @param      {THREE.Object3D}  node  The 3d tile node
 * @return     {boolean}
 */
function $3dTilesSubdivisionControl(context, layer, node) {
  if (layer.tileset.tiles[node.tileId].children === undefined) {
    return false;
  }
  if (layer.tileset.tiles[node.tileId].isTileset) {
    return true;
  }
  const sse = computeNodeSSE(context.camera, node);
  return sse > layer.sseThreshold;
}