/* globals window, _, VIZI */

/**
 * Main entry point
 * @author Robin Hawkes - vizicities.com
 */

(function() {
  "use strict";

  VIZI.World = function(options) {
    if (VIZI.DEBUG) console.log("Initialising VIZI.World");

    var self = this;

    self.options = options || {};
    
    _.defaults(self.options, {
      crs: VIZI.CRS.EPSG3857,
      center: new VIZI.LatLon(51.50358, -0.01924),
      zoom: 16,
      suppressRenderer: false, // Set true for tests
      layersUI: true,
      picking: false,
      antialias: false
    });

    if (!self.options.viewport) {
      throw new Error("Required viewport option missing");
    }

    self.crs = self.options.crs;

    self.attribution = new VIZI.Attribution({
      element: self.options.viewport
    });

    // TODO: Store switchboards and layers in an id-referenced object
    self.switchboards = [];
    self.layers = [];

    // Add UI container
    // TODO: Move into a separate UI class
    var uiContainer = document.createElement("div");

    // Styling is adding via vizicities.css
    uiContainer.classList.add("vizicities-ui");
    self.options.viewport.appendChild(uiContainer);

    // Set up layer UI
    if (self.options.layersUI) {
      self.layersUI = new VIZI.LayersUI(self.layers);
    }

    // TODO: Ability to override this with a scene passed into the options
    // TODO: Pass-through options that tweak scene (antialias, etc)
    self.scene = new VIZI.Scene({
      antialias: self.options.antialias,
      viewport: self.options.viewport,
      // TODO: Remove this when running WebGL tests on Travis is solved
      suppressRenderer: self.options.suppressRenderer,
      picking: self.options.picking
    });

    self.camera = self.options.camera || new VIZI.Camera({
      aspect: self.options.viewport.clientWidth / self.options.viewport.clientHeight
    });

    self.camera.addToScene(self.scene);

    // Origins are used as a fixed base for position projections
    self.origin = new VIZI.LatLon(self.options.center);
    self.originZoom = self.options.zoom;

    // Zoom and center are a dynamic representation of the current state
    // These don't affect the values returned when projecting positions
    self.zoom = undefined;
    self.center = undefined;

    self.updateView(self.options.center, self.options.zoom);

    // Window resize
    window.addEventListener("resize", function(event) {
      self.resizeView(self.options.viewport.clientWidth, self.options.viewport.clientHeight);
    });

    VIZI.Messenger.on("controls:move", function(point) {
      // TODO: Should be more intelligent about whether this has changed
      var unprojected = self.unproject(point);
      self.updateView(unprojected);
    });

    VIZI.Messenger.on("controls:zoom", function(distance) {
      // Convert control zoom pixel distance to map zoom
      // TODO: Work out a way to use meters instead of pixels (or not needed?)
      var zoom = Math.ceil(self.crs.altitudeToZoom(distance));

      // TODO: Should be more intelligent about whether this has changed
      self.zoomTo(zoom);
    });
  };

  VIZI.World.prototype.project = function(latLon, zoom) {
    var self = this;
    zoom = zoom || self.originZoom;

    // TODO: Are there ramifications to rounding the pixels?
    var originPoint = self.crs.latLonToPoint(self.origin, zoom, {round: false});
    var projected = self.crs.latLonToPoint(latLon, zoom, {round: false});

    return projected.clone().subtract(originPoint);
  };

  VIZI.World.prototype.unproject = function(point, zoom) {
    var self = this;
    zoom = zoom || self.originZoom;

    // TODO: Are there ramifications to rounding the pixels?
    var originPoint = self.crs.latLonToPoint(self.origin, zoom, {round: false});

    return self.crs.pointToLatLon(point.clone().add(originPoint), zoom);
  };

  VIZI.World.prototype.pixelsPerMeter = function(latLon, zoom) {
    var self = this;
    zoom = zoom || self.originZoom;
    
    return self.crs.pixelsPerMeter(latLon, zoom);
  };

  VIZI.World.prototype.addLayer = function(layer) {
    var self = this;

    self.layers.push(layer);
    self.scene.add(layer.object);

    // Update layers UI
    if (self.layersUI) {
      self.layersUI.onChange();
    }
  };

  VIZI.World.prototype.addPickable = function(mesh, id) {
    var self = this;

    if (!self.options.picking) {
      return;
    }

    self.scene.addPickable(mesh, id);
  };

  VIZI.World.prototype.addSwitchboard = function(switchboard) {
    var self = this;

    self.switchboards.push(switchboard);
  };

  // Update world and blueprint states on each frame
  VIZI.World.prototype.onTick = function(delta) {
    var self = this;

    // REMOVED: As new NoFlo approach doesn't use switchboards
    // _.each(self.switchboards, function(switchboard) {
    //   switchboard.onTick(delta);
    // });
  
    _.each(self.layers, function(layer) {
      layer.onTick(delta);
    });
  };

  // Render current world state
  VIZI.World.prototype.render = function() {
    var self = this;
    self.scene.render(self.camera);
  };

  // Centralised method to handle variable changes and firing of events
  // TODO: Trigger events as move and zoom progress
  VIZI.World.prototype.updateView = function(center, zoom) {
    var self = this;

    if (zoom) {
      self.zoom = zoom;
    }

    self.center = center;

    // This will trigger things like a grid update within BlueprintOutput
    VIZI.Messenger.emit("world:updateView", self.center, self.zoom);
  };

  VIZI.World.prototype.resizeView = function(width, height) {
    var self = this;

    var aspect = width / height;
    self.camera.changeAspect(aspect);

    self.scene.resize(width, height);
  };

  // REMOVED: These don't seem to be of any use in world
  // It makes more sense to change camera movement via the controls
  // These helpers would make more sense if a reference to controls was
  // stored in the world class.

  // VIZI.World.prototype.moveToLatLon = function(latLon) {
  //   var self = this;
  //   self.updateView(latLon);
  // };

  // VIZI.World.prototype.moveToPoint = function(point) {
  //   var self = this;
  //   // TODO: Are there ramifications to not rounding the pixels?
  //   var unprojected = self.unproject(point);
  //   self.updateView(unprojected);
  // };

  // VIZI.World.prototype.moveBy = function(point) {
  //   var self = this;
  //   // TODO: Are there ramifications to not rounding the pixels?
  //   var centerProjected = self.crs.latLonToPoint(self.center, self.zoom);
  //   var newPoint = centerProjected.clone().add(point);
  //   self.updateView(self.crs.pointToLatLon(newPoint, self.zoom));
  // };

  // TODO: Should this be handled within the controls?
  // What purpose does it serve here if it's not changing the control zoom?
  VIZI.World.prototype.zoomTo = function(zoom) {
    var self = this;
    self.updateView(self.center, zoom);
  };

  // TODO: Should this be handled within the controls?
  // What purpose does it serve here if it's not changing the control zoom?
  VIZI.World.prototype.zoomIn = function(delta) {
    var self = this;
    self.updateView(self.center, self.zoom + delta);
  };

  // TODO: Should this be handled within the controls?
  // What purpose does it serve here if it's not changing the control zoom?
  VIZI.World.prototype.zoomOut = function(delta) {
    var self = this;
    self.updateView(self.center, self.zoom - delta);
  };

  // TODO: Trigger events as camera change progresses
  // TODO: Should this be handled within the controls?
  // What purpose does it serve here?
  VIZI.World.prototype.lookAtLatLon = function(latLon) {
    var self = this;
    var projected = self.project(latLon);
    self.camera.lookAt(projected);
  };

  // TODO: Trigger events as camera change progresses
  // TODO: Should this be handled within the controls?
  // What purpose does it serve here?
  VIZI.World.prototype.lookAtPoint = function(point) {
    var self = this;

    self.camera.lookAt(point);
  };

  // From: http://stackoverflow.com/a/27412386/997339
  VIZI.World.prototype.worldPositionTo2D = function(position) {
    var self = this;

    var vector3 = position.clone();

    // Map to normalized device coordinate (NDC) space
    vector3.project(self.camera.camera);

    // Map to 2D screen space
    var position2D = new VIZI.Point();
    position2D.x = Math.round((vector3.x + 1) * self.options.viewport.clientWidth / 2),
    position2D.y = Math.round((-vector3.y + 1) * self.options.viewport.clientHeight / 2);
    
    return position2D;
  };
})();