import {point as Point} from '../../geo/Point';
import {latLon as LatLon} from '../../geo/LatLon';
import THREE from 'three';

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

// Manages a single tile and its layers

var r2d = 180 / Math.PI;

var tileURLRegex = /\{([szxy])\}/g;

class Tile {
  constructor(quadcode, path, layer) {
    this._layer = layer;
    this._world = layer._world;
    this._quadcode = quadcode;
    this._path = path;

    this._ready = false;
    this._aborted = false;

    this._tile = this._quadcodeToTile(quadcode);

    // Bottom-left and top-right bounds in WGS84 coordinates
    this._boundsLatLon = this._tileBoundsWGS84(this._tile);

    // Bottom-left and top-right bounds in world coordinates
    this._boundsWorld = this._tileBoundsFromWGS84(this._boundsLatLon);

    // Tile center in world coordinates
    this._center = this._boundsToCenter(this._boundsWorld);

    // Tile center in projected coordinates
    this._centerLatlon = this._world.pointToLatLon(Point(this._center[0], this._center[1]));

    // Length of a tile side in world coorindates
    this._side = this._getSide(this._boundsWorld);

    // Point scale for tile (for unit conversion)
    this._pointScale = this._world.pointScale(this._centerLatlon);
  }

  // Returns true if the tile mesh and texture are ready to be used
  // Otherwise, returns false
  isReady() {
    return this._ready;
  }

  isAborted() {
    return this._aborted;
  }

  // Request data for the tile
  requestTileAsync() {}

  getQuadcode() {
    return this._quadcode;
  }

  getBounds() {
    return this._boundsWorld;
  }

  getCenter() {
    return this._center;
  }

  getSide() {
    return this._side;
  }

  getMesh() {
    return this._mesh;
  }

  getPickingMesh() {
    return this._pickingMesh;
  }

  // Destroys the tile and removes it from the layer and memory
  //
  // Ensure that this leaves no trace of the tile – no textures, no meshes,
  // nothing in memory or the GPU
  destroy() {
    // console.log('Destroying tile', this._quadcode);

    // Delete reference to layer and world
    this._layer = null;
    this._world = null;

    // Delete location references
    this._boundsLatLon = null;
    this._boundsWorld = null;
    this._center = null;

    // Done if no mesh
    if (!this._mesh && !this._pickingMesh) {
      return;
    }

    this.destroyMesh(this._mesh);
    this.destroyMesh(this._pickingMesh);

    this._mesh = null;
    this._pickingMesh = null;
  }

  destroyMesh(mesh, dispose = true) {
    if (mesh) {
      if (mesh.children) {
        mesh.children.forEach((child) => {
          mesh.remove(child);
          this.destroyMesh(child);
        });
      }

      if (dispose) {
        if (mesh.geometry) {
          mesh.geometry.dispose();
          mesh.geometry = null;
        }

        if (mesh.material) {
          if (mesh.material.map) {
            mesh.material.map.dispose();
            mesh.material.map = null;
          }

          mesh.material.dispose();
          mesh.material = null;
        }
      }
    }
  }

  _createMesh() {}
  _createDebugMesh() {}

  _getTileURL(urlParams) {
    if (!urlParams.s) {
      // Default to a random choice of a, b or c
      urlParams.s = String.fromCharCode(97 + Math.floor(Math.random() * 3));
    }

    tileURLRegex.lastIndex = 0;
    return this._path.replace(tileURLRegex, function(value, key) {
      // Replace with paramter, otherwise keep existing value
      return urlParams[key];
    });
  }

  // Convert from quadcode to TMS tile coordinates
  _quadcodeToTile(quadcode) {
    var x = 0;
    var y = 0;
    var z = quadcode.length;

    for (var i = z; i > 0; i--) {
      var mask = 1 << (i - 1);
      var q = +quadcode[z - i];
      if (q === 1) {
        x |= mask;
      }
      if (q === 2) {
        y |= mask;
      }
      if (q === 3) {
        x |= mask;
        y |= mask;
      }
    }

    return [x, y, z];
  }

  // Convert WGS84 tile bounds to world coordinates
  _tileBoundsFromWGS84(boundsWGS84) {
    var sw = this._layer._world.latLonToPoint(LatLon(boundsWGS84[1], boundsWGS84[0]));
    var ne = this._layer._world.latLonToPoint(LatLon(boundsWGS84[3], boundsWGS84[2]));

    return [sw.x, sw.y, ne.x, ne.y];
  }

  // Get tile bounds in WGS84 coordinates
  _tileBoundsWGS84(tile) {
    var e = this._tile2lon(tile[0] + 1, tile[2]);
    var w = this._tile2lon(tile[0], tile[2]);
    var s = this._tile2lat(tile[1] + 1, tile[2]);
    var n = this._tile2lat(tile[1], tile[2]);
    return [w, s, e, n];
  }

  _tile2lon(x, z) {
    return x / Math.pow(2, z) * 360 - 180;
  }

  _tile2lat(y, z) {
    var n = Math.PI - 2 * Math.PI * y / Math.pow(2, z);
    return r2d * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
  }

  _boundsToCenter(bounds) {
    var x = bounds[0] + (bounds[2] - bounds[0]) / 2;
    var y = bounds[1] + (bounds[3] - bounds[1]) / 2;

    return [x, y];
  }

  _getSide(bounds) {
    return (new THREE.Vector3(bounds[0], 0, bounds[3])).sub(new THREE.Vector3(bounds[0], 0, bounds[1])).length();
  }
}

export default Tile;
