"use strict";

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Handles picking of the world objects under the mouse pointer - uses common technique of drawing
 * each object in different color.
 *
 * @author smrceant (Antonin Smrcek)
 *
 * @constructor
 */
var Picking = function()
{
  var _this = this;

  /**
   * Contents (pixels) of _pickingRenderMapTexture, which can be accessed like a normal array. After the contents of
   * the texture are pushed there using glReadPixels, we can detect the pixel under the mouse.
   *
   * @private
   * @type {Uint8Array}
   */
  this._lastColorBuffer = null;

  /**
   * Reinitializes the color buffer based on the value which provides the rendering window.
   */
  var initLastColorBuffer = function()
  {
    _this._lastColorBuffer = new Uint8Array (appMain.getRenderingWindow().windowPixelCountRGBA);
  };
  initLastColorBuffer();

  appMain.getRenderingWindow().addResizingCallback (function()
  {
    // init color buffer only if the new pixel count is bigger than our current color buffer, i.e. new resolution is bigger
    // than the current one (otherwise when it is smaller it is ok, we don't need to fill the entire buffer)
    if (appMain.getRenderingWindow().windowPixelCountRGBA > _this._lastColorBuffer.length)
    {
      initLastColorBuffer();
    }
  });

  /**
   * Texture (off-screen buffer) into which is pick draw (= scene with models that have some unique colors instead of textures on them) rendered.
   *
   * @private
   * @type {THREE.WebGLRenderTarget}
   */
  this._pickingRenderMapTexture = new THREE.WebGLRenderTarget (appMain.getRenderingWindow().windowWidth, appMain.getRenderingWindow().windowHeight,
      {
        minFilter: THREE.LinearFilter, // or use nearest?
        magFilter: THREE.NearestFilter,
        format:    THREE.RGBAFormat
      });
  appMain.getRenderingWindow().updateObjectAfterResize (this._pickingRenderMapTexture.setSize.bind (this._pickingRenderMapTexture));

  /**
   * Was the picking color buffer drawn for current view (i.e. _preparePick() was called)?
   *
   * @private
   * @type {boolean}
   */
  this._pickingMapDrawnForView = false;

  /**
   * Object under the mouse - name (what will be displayed as string).
   *
   * @type {string}
   */
  this.currentPickableObject = null;

  /**
   * Normally the increment is 1, but then the picking framebuffer contents (colors) won't be distinguishable by naked eyes.
   * When the flag is set, then we increment by 100, which makes sure that colors are at least little different.
   * You can of course change this value to much bigger one. Just remember that the biggest one can be MAXIMUM_PICKING_ID high.
   * Which is like 16 000 000, so you should be alright even with big increments.
   *
   * @private
   * @type {number}
   */
  this._pickingIdIncrement = Preferences.Picking.ASSIGN_PICKING_COLORS_DISTINGUISHABLE_BY_EYES ? 100 : 1;

  /**
   * Next picking id which will be assigned for picking via function _assignPickingIds().
   *
   * IMPORTANT: Cannot ever be 0, since 0 is reserved for non-pickable objects. If you make it zero, you will suffer.
   *
   * @type {!number}
   */
  this._currentPickingId = this._pickingIdIncrement;

  /**
   * Mapping between pickingId and name of the group from Sketchup, e.g. 1 -> "chapel". Essentially, by looking into this map, we can
   * retrieve name of the picked model and display its information.
   *
   * How to query this object: the keys are pickable group names, if it returns undefined, then the group was not defined in SketchUp,
   * if it returns null, then you do something wrong, because although null is valid return values, it is assigned to the
   * first (0th) element as dummy for non-pickable objects. If it returns something, it is name of the pickable group defined in SketchUp.
   *
   * @private
   * @type {string}
   */
  this._pickingIdToPickableGroupNameMap = {};

  /**
   * Inverse mapping to pickingIdToPickableGroupNameMap, e.g. "chapel" -> 1
   *
   * @type {number}
   */
  this.pickableGroupNameToPickingIdMap = {};
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// PICKING ASSIGNMENT
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Assigns picking ids for pickable models in the scene or for a specific model. These models then will be pickable by user (by hovering/clicking with mouse).
 *
 * This method can be called multiple times if it is necessary to assign picking IDs in several "phases".
 *
 * @param   {THREE.Object3D|THREE.Mesh}  pickableModels     collection of all pickable groups with models (pickable group = all models in SketchUp which were assigned to a group with name e.g. like "pickable_gate"); it can be also just single THREE.Mesh instance (this is used when we want to generate IDs for info spinning cubes which are not associated with any 3D world object)
 * @returns {number}                                        which picking ID was assigned as last by this method; useful when pickableModels passed in is a THREE.Mesh instance
 */
Picking.prototype.assignPickingIds = function (pickableModels)
{
  var _this = this;

  var pickableObjects = new THREE.Object3D();

  // IMPORTANT! Since pickingId 0 corresponds to no object (not pickable), also our first (0-th) object in the array must be null for all to work.
  // (note: this is technically called multiple times when the method is called multiple times, but it doesn't matter)
  this._pickingIdToPickableGroupNameMap[0] = null;

  for (var i = 0; i < pickableModels.children.length; i++)
  {
    var child = pickableModels.children[i];

    if(!ContentLibrary.getContentString(child.name)) {
        continue;
    }

    this._pickingIdToPickableGroupNameMap[this._currentPickingId] = child.name;
    this.pickableGroupNameToPickingIdMap[ this._pickingIdToPickableGroupNameMap[this._currentPickingId] ] = this._currentPickingId;

    // create a dialog with information about pickable object
    ObjectInfo.createObjectInfo (this._pickingIdToPickableGroupNameMap[this._currentPickingId]);

    // find all Meshes in hierarchy of current pickable group (= children[i]) and assign them same picking id
    child.traverse(function (object) {
        if (object instanceof THREE.Mesh) {
          // (note: at this moment, the Mesh's geometry is (must be) still of type THREE.Geometry, not THREE.BufferGeometry!
          object.userData.pickingId = _this._currentPickingId;
        }
    });

    // increment picking id by configured amount (naturally it can be incremented by 1 using ++, but then we won't be able to see contents of picking buffer with naked eyes, so that's why there is a way how to change value of the increment)
    this._currentPickingId += this._pickingIdIncrement;

    pickableObjects.children.push (child);
  }

  return pickableObjects;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


/**
 * Tries to execute picking. This is the high-Handles displaying/hiding picking information.
 */
Picking.prototype.checkPickingState = function()
{
  if (!Preferences.Picking.PICKING_ALLOWED)
  {
    return;
  }

    // if picking map was not computed for this view, compute it
    if (!this._pickingMapDrawnForView)
    {
      //console.log("Picking performed! At: " + Date.now());
      this._pickingMapDrawnForView = true;
      this._preparePick();
    }

    if (!control.mouseOutOfCanvas)
    {
      // perform picking
      this.currentPickableObject = this._pick();
      gui.showPickableModelNameUnderMouse (this.currentPickableObject);
    }
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MAPPING FUNCTION COLOR <-> ID
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Used when reading pick draw information and determining what object was under the mouse cursor.
 * Since we use same algorithm and principle as for depth encoding, we just call the appropriate method of renderer.
 *
 * Inverse function to _computeColorFromId().
 *
 * @param   {number[]}     color RGBA color vector (but only RGB portions matter)
 * @returns {number}                  integer identificator which was encoded into color
 * @private
 */
Picking.prototype._computeIdFromColor = function (color)
{
  return renderer.unpackDepth24 (color);
};

/**
 * Computing color which corresponds to the given picking id. Inverse function to _computeIdFromColor().
 * The passed pickingId is a float number, not an integer! This is because of our number-into-pixels encoding algorithm.
 *
 * @param   {number}    pickingId   float(!!!) identificator of the object
 * @returns {number[]}              id of the object (integer) encoded into a color
 */
Picking.prototype._computeColorFromId = function (pickingId)
{
  return renderer.packDepth24 (pickingId);
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MAKING/INVALIDATING PICKING MAP, PICKING EXECUTION
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Sets flag indicating that current picking map can be no longer used (happens when view has changed).
 */
Picking.prototype.invalidatePickingMap = function()
{
  this._pickingMapDrawnForView = false;
};

/**
 * Renders the scene in picking colors of the objects and pushes content of the rendered image into the array from
 * which we can read the pixel values.
 *
 * @private
 */
Picking.prototype._preparePick = function()
{
  renderer.renderPick (this._pickingRenderMapTexture);

  // "screenshot", i.e. save state of the offscreen framebuffer where objects were rendered in pick colors
  renderer.getRendererThree().readRenderTargetPixels (this._pickingRenderMapTexture, 0, 0, appMain.getRenderingWindow().windowWidth, appMain.getRenderingWindow().windowHeight, this._lastColorBuffer);
};

/**
 * Checks if some object is under the mouse and if it is, returns its name.
 * This method must be called only if we have up-to-date offscreen color buffer with picking colors.
 *
 * @private
 * @returns {string}    name of the object under the mouse
 */
Picking.prototype._pick = function()
{
  control.computeCanvasMouseCoordinates();

  var pickedModel = this.getPickedGroupWithModelsFromColor
  (
      Renderer.readPixelColor (
          this._lastColorBuffer,
          appMain.getRenderingWindow().windowWidth, appMain.getRenderingWindow().windowHeight,
          control.mouseCanvas.x, control.mouseCanvas.y
      )
  );

  return pickedModel;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CONVERTING PICK COLOR TO OBJECT
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Finds out which object (or rather group of objects, but group in a sense of "collection of faces") correspond
 * to the given color.
 *
 * @private
 * @param   {number[]}      pickedColor
 * @returns {string}        model name which corresponds to given color
 */
Picking.prototype.getPickedGroupWithModelsFromColor = function (pickedColor)
{
  var pickedObjectIdFloat = this._computeIdFromColor (pickedColor);
  var pickedObjectIdInteger = Preferences.Picking.getPickingIdInIntegerFullRange (pickedObjectIdFloat);

  //console.log("pickedObjectIdFloat:"+pickedObjectIdFloat);
  //console.log("pickedObjectIdInteger:"+pickedObjectIdInteger);

  return this._pickingIdToPickableGroupNameMap[pickedObjectIdInteger];
};
