import EventEmitter from 'eventemitter3';
import extend from 'lodash.assign';
import shortid from 'shortid';
import THREE from 'three';
import Scene from '../engine/Scene';
import {CSS3DObject} from '../vendor/CSS3DRenderer';
import {CSS2DObject} from '../vendor/CSS2DRenderer';

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

// TODO: Need a single move method that handles moving all the various object
// layers so that the DOM layers stay in sync with the 3D layer

// TODO: Double check that objects within the _object3D Object3D parent are frustum
// culled even if the layer position stays at the default (0,0,0) and the child
// objects are positioned much further away
//
// Or does the layer being at (0,0,0) prevent the child objects from being
// culled because the layer parent is effectively always in view even if the
// child is actually out of camera

class Layer extends EventEmitter {
  constructor(options) {
    super();

    var defaults = {
      id: shortid.generate(),
      output: true,
      outputToScene: true
    };

    this._options = extend({}, defaults, options);

    if (this.isOutput()) {
      this._object3D = new THREE.Object3D();

      this._dom3D = document.createElement('div');
      this._domObject3D = new CSS3DObject(this._dom3D);

      this._dom2D = document.createElement('div');
      this._domObject2D = new CSS2DObject(this._dom2D);
    }
  }

  // Add THREE object directly to layer
  add(object) {
    this._object3D.add(object);
  }

  // Remove THREE object from to layer
  remove(object) {
    this._object3D.remove(object);
  }

  addDOM3D(object) {
    this._domObject3D.add(object);
  }

  removeDOM3D(object) {
    this._domObject3D.remove(object);
  }

  addDOM2D(object) {
    this._domObject2D.add(object);
  }

  removeDOM2D(object) {
    this._domObject2D.remove(object);
  }

  // Add layer to world instance and store world reference
  addTo(world) {
    return world.addLayer(this);
  }

  // Internal method called by World.addLayer to actually add the layer
  _addToWorld(world) {
    this._world = world;

    return new Promise((resolve, reject) => {
      this._onAdd(world).then(() => {
        this.emit('added');
        resolve(this);
      }).catch(reject);
    });
  }

  // Must return a promise
  _onAdd(world) {
    return Promise.resolve(this);
  }

  getPickingId() {
    if (this._world._engine._picking) {
      return this._world._engine._picking.getNextId();
    }

    return false;
  }

  // TODO: Tidy this up and don't access so many private properties to work
  addToPicking(object) {
    if (!this._world._engine._picking) {
      return;
    }

    this._world._engine._picking.add(object);
  }

  removeFromPicking(object) {
    if (!this._world._engine._picking) {
      return;
    }

    this._world._engine._picking.remove(object);
  }

  isOutput() {
    return this._options.output;
  }

  isOutputToScene() {
    return this._options.outputToScene;
  }

  // TODO: Also hide any attached DOM layers
  hide() {
    this._object3D.visible = false;

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

  // TODO: Also show any attached DOM layers
  show() {
    this._object3D.visible = true;

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

  // Destroys the layer and removes it from the scene and memory
  destroy() {
    if (this._object3D && this._object3D.children) {
      // Remove everything else in the layer
      var child;
      for (var i = this._object3D.children.length - 1; i >= 0; i--) {
        child = this._object3D.children[i];

        if (!child) {
          continue;
        }

        this.remove(child);

        if (child.geometry) {
          // Dispose of mesh and materials
          child.geometry.dispose();
          child.geometry = null;
        }

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

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

    if (this._domObject3D && this._domObject3D.children) {
      // Remove everything else in the layer
      var child;
      for (var i = this._domObject3D.children.length - 1; i >= 0; i--) {
        child = this._domObject3D.children[i];

        if (!child) {
          continue;
        }

        this.removeDOM3D(child);
      }
    }

    if (this._domObject2D && this._domObject2D.children) {
      // Remove everything else in the layer
      var child;
      for (var i = this._domObject2D.children.length - 1; i >= 0; i--) {
        child = this._domObject2D.children[i];

        if (!child) {
          continue;
        }

        this.removeDOM2D(child);
      }
    }

    this._domObject3D = null;
    this._domObject2D = null;

    this._world = null;
    this._object3D = null;
  }
}

export default Layer;

var noNew = function(options) {
  return new Layer(options);
};

export {noNew as layer};
