import Layer from '../Layer';
import extend from 'lodash.assign';
import TileCache from './TileCache';
import THREE from 'three';

// TODO: Consider removing picking from TileLayer instances as there aren't
// (m)any situations where it would be practical
//
// For example, how would you even know what picking IDs to listen to and what
// to do with them?

// TODO: Make sure nothing is left behind in the heap after calling destroy()

// TODO: Consider keeping a single TileLayer / LOD instance running by default
// that keeps a standard LOD grid for other layers to utilise, rather than
// having to create their own, unique LOD grid and duplicate calculations when
// they're going to use the same grid setup anyway
//
// It still makes sense to be able to have a custom LOD grid for some layers as
// they may want to customise things, maybe not even using a quadtree at all!
//
// Perhaps it makes sense to split out the quadtree stuff into a singleton and
// pass in the necessary parameters each time for the calculation step.
//
// Either way, it seems silly to force layers to have to create a new LOD grid
// each time and create extra, duplicated processing every frame.

// TODO: Allow passing in of options to define min/max LOD and a distance to use
// for culling tiles beyond that distance.

// DONE: Prevent tiles from being loaded if they are further than a certain
// distance from the camera and are unlikely to be seen anyway

// TODO: Avoid performing LOD calculation when it isn't required. For example,
// when nothing has changed since the last frame and there are no tiles to be
// loaded or in need of rendering

// TODO: Only remove tiles from the layer that aren't to be rendered in the
// current frame – it seems excessive to remove all tiles and re-add them on
// every single frame, even if it's just array manipulation

// TODO: Fix LOD calculation so min and max LOD can be changed without causing
// problems (eg. making min above 5 causes all sorts of issues)

// TODO: Reuse THREE objects where possible instead of creating new instances
// on every LOD calculation

// TODO: Consider not using THREE or LatLon / Point objects in LOD calculations
// to avoid creating unnecessary memory for garbage collection

// TODO: Prioritise loading of tiles at highest level in the quadtree (those
// closest to the camera) so visual inconsistancies during loading are minimised

class TileLayer extends Layer {
  constructor(options) {
    var defaults = {
      picking: false,
      maxCache: 1000,
      maxLOD: 18
    };

    var _options = extend({}, defaults, options);

    super(_options);

    this._destroy = false;

    this._tileCache = new TileCache(this._options.maxCache, tile => {
      this._destroyTile(tile);
    });

    // List of tiles from the previous LOD calculation
    this._tileList = [];

    // TODO: Work out why changing the minLOD causes loads of issues
    this._minLOD = 3;
    this._maxLOD = this._options.maxLOD;

    this._frustum = new THREE.Frustum();
    this._tiles = new THREE.Object3D();
    this._tilesPicking = new THREE.Object3D();
  }

  _onAdd(world) {
    this.addToPicking(this._tilesPicking);
    this.add(this._tiles);

    return Promise.resolve();
  }

  _updateFrustum() {
    var camera = this._world.getCamera();
    var projScreenMatrix = new THREE.Matrix4();
    projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);

    this._frustum.setFromMatrix(camera.projectionMatrix);
    this._frustum.setFromMatrix(new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse));
  }

  _tileInFrustum(tile) {
    var bounds = tile.getBounds();
    return this._frustum.intersectsBox(new THREE.Box3(new THREE.Vector3(bounds[0], 0, bounds[3]), new THREE.Vector3(bounds[2], 0, bounds[1])));
  }

  // Update and output tiles from the previous LOD checklist
  _outputTiles() {
    if (!this._tiles || this._destroy) {
      return;
    }

    // Remove all tiles from layer
    this._removeTiles();

    // Add / re-add tiles
    this._tileList.forEach(tile => {
      // Are the mesh and texture ready?
      //
      // If yes, continue
      // If no, skip
      if (!tile.isReady()) {
        return;
      }

      // Add tile to layer (and to scene) if not already there
      this._tiles.add(tile.getMesh());

      if (tile.getPickingMesh()) {
        this._tilesPicking.add(tile.getPickingMesh());
      }
    });

    // Emit event notifying of new tiles
    this.emit('tilesList', this._tileList.map((tile) => tile));
  }

  // Works out tiles in the view frustum and stores them in an array
  //
  // Does not output the tiles, deferring this to _outputTiles()
  _calculateLOD() {
    if (this._stop || !this._world || this._destroy) {
      return;
    }

    // var start = (performance || Date).now();

    var camera = this._world.getCamera();

    // 1. Update and retrieve camera frustum
    this._updateFrustum(this._frustum, camera);

    // 2. Add the four root items of the quadtree to a check list
    var checkList = this._checklist;
    checkList = [];
    checkList.push(this._requestTile('0', this));
    checkList.push(this._requestTile('1', this));
    checkList.push(this._requestTile('2', this));
    checkList.push(this._requestTile('3', this));

    // 3. Call Divide, passing in the check list
    this._divide(checkList);

    // // 4. Remove all tiles from layer
    //
    // Moved to _outputTiles() for now
    // this._removeTiles();

    // Order tile-list by zoom so nearest tiles are requested first
    checkList.sort((a, b) => {
      return a._quadcode.length < b._quadcode.length;
    });

    // 5. Filter the tiles remaining in the check list
    var tileList = checkList.filter((tile, index) => {
      // Skip tile if it's not in the current view frustum
      if (!this._tileInFrustum(tile)) {
        return false;
      }

      if (this._options.distance && this._options.distance > 0) {
        // TODO: Can probably speed this up
        var center = tile.getCenter();
        var dist = (new THREE.Vector3(center[0], 0, center[1])).sub(camera.position).length();

        // Manual distance limit to cut down on tiles so far away
        if (dist > this._options.distance) {
          return false;
        }
      }

      // Does the tile have a mesh?
      //
      // If yes, continue
      // If no, generate tile mesh, request texture and skip
      if (!tile.getMesh() || tile.isAborted()) {
        tile.requestTileAsync();
      }

      return true;

      // Are the mesh and texture ready?
      //
      // If yes, continue
      // If no, skip
      // if (!tile.isReady()) {
      //   return;
      // }
      //
      // // Add tile to layer (and to scene)
      // this._tiles.add(tile.getMesh());
    });

    // Get list of tiles that were in the previous update but not the
    // current one (for aborting requests, etc)
    var missingTiles = this._tileList.filter((item) => {
      return tileList.indexOf(item) < 0;
    });

    // Abort tiles that are no longer in view
    missingTiles.forEach((tile) => tile._abortRequest());

    this._tileList = tileList;

    // console.log((performance || Date).now() - start);
  }

  _divide(checkList) {
    var count = 0;
    var currentItem;
    var quadcode;

    // 1. Loop until count equals check list length
    while (count != checkList.length) {
      currentItem = checkList[count];
      quadcode = currentItem.getQuadcode();

      // 2. Increase count and continue loop if quadcode equals max LOD / zoom
      if (currentItem.length === this._maxLOD) {
        count++;
        continue;
      }

      // 3. Else, calculate screen-space error metric for quadcode
      if (this._screenSpaceError(currentItem)) {
        // 4. If error is sufficient...

        // 4a. Remove parent item from the check list
        checkList.splice(count, 1);

        // 4b. Add 4 child items to the check list
        checkList.push(this._requestTile(quadcode + '0', this));
        checkList.push(this._requestTile(quadcode + '1', this));
        checkList.push(this._requestTile(quadcode + '2', this));
        checkList.push(this._requestTile(quadcode + '3', this));

        // 4d. Continue the loop without increasing count
        continue;
      } else {
        // 5. Else, increase count and continue loop
        count++;
      }
    }
  }

  _screenSpaceError(tile) {
    var minDepth = this._minLOD;
    var maxDepth = this._maxLOD;

    var quadcode = tile.getQuadcode();

    var camera = this._world.getCamera();

    // Tweak this value to refine specific point that each quad is subdivided
    //
    // It's used to multiple the dimensions of the tile sides before
    // comparing against the tile distance from camera
    var quality = 3.0;

    // 1. Return false if quadcode length equals maxDepth (stop dividing)
    if (quadcode.length === maxDepth) {
      return false;
    }

    // 2. Return true if quadcode length is less than minDepth
    if (quadcode.length < minDepth) {
      return true;
    }

    // 3. Return false if quadcode bounds are not in view frustum
    if (!this._tileInFrustum(tile)) {
      return false;
    }

    var center = tile.getCenter();

    // 4. Calculate screen-space error metric
    // TODO: Use closest distance to one of the 4 tile corners
    var dist = (new THREE.Vector3(center[0], 0, center[1])).sub(camera.position).length();

    var error = quality * tile.getSide() / dist;

    // 5. Return true if error is greater than 1.0, else return false
    return (error > 1.0);
  }

  _removeTiles() {
    if (!this._tiles || !this._tiles.children) {
      return;
    }

    for (var i = this._tiles.children.length - 1; i >= 0; i--) {
      this._tiles.remove(this._tiles.children[i]);
    }

    if (!this._tilesPicking || !this._tilesPicking.children) {
      return;
    }

    for (var i = this._tilesPicking.children.length - 1; i >= 0; i--) {
      this._tilesPicking.remove(this._tilesPicking.children[i]);
    }
  }

  // Return a new tile instance
  _createTile(quadcode, layer) {}

  // Get a cached tile or request a new one if not in cache
  _requestTile(quadcode, layer) {
    var tile = this._tileCache.getTile(quadcode);

    if (!tile) {
      // Set up a brand new tile
      tile = this._createTile(quadcode, layer);

      // Add tile to cache, though it won't be ready yet as the data is being
      // requested from various places asynchronously
      this._tileCache.setTile(quadcode, tile);
    }

    return tile;
  }

  _destroyTile(tile) {
    // Remove tile from scene
    this._tiles.remove(tile.getMesh());

    // Delete any references to the tile within this component

    // Call destory on tile instance
    tile.destroy();
  }

  show() {
    this._stop = false;

    if (this._tilesPicking) {
      this._tilesPicking.visible = true;
    }

    this._calculateLOD();
    super.show();
  }

  hide() {
    this._stop = true;

    if (this._tilesPicking) {
      this._tilesPicking.visible = false;
    }

    super.hide();
  }

  // Destroys the layer and removes it from the scene and memory
  destroy() {
    this._destroy = true;

    if (this._tiles.children) {
      // Remove all tiles
      for (var i = this._tiles.children.length - 1; i >= 0; i--) {
        this._tiles.remove(this._tiles.children[i]);
      }
    }

    // Remove tile from picking scene
    this.removeFromPicking(this._tilesPicking);

    if (this._tilesPicking.children) {
      // Remove all tiles
      for (var i = this._tilesPicking.children.length - 1; i >= 0; i--) {
        this._tilesPicking.remove(this._tilesPicking.children[i]);
      }
    }

    this._tileCache.destroy();
    this._tileCache = null;

    this._tiles = null;
    this._tilesPicking = null;
    this._frustum = null;

    super.destroy();
  }
}

export default TileLayer;
