"use strict";

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

/**
 * Contains various method associated with rendering. Uses underlying Three.js WebGLRenderer to actually render things.
 *
 * @author smrceant (Antonin Smrcek)
 *
 * @constructor
 */
var Renderer = function()
{
  /**
   * Main renderer.
   *
   * @type {THREE.WebGLRenderer}
   */
  this.rendererThree =  new THREE.WebGLRenderer
  ({
    canvas: canvas,
    antialias: !Preferences.Heightmap.GENERATE_HEIGHTMAP && Preferences.VisualEffects.MSAA,
    preserveDrawingBuffer: true,
  });
  this.rendererThree.setPixelRatio (window.devicePixelRatio);
  this.rendererThree.setSize (Preferences.Renderer.RENDER_WIDTH, Preferences.Renderer.RENDER_HEIGHT);
  this.rendererThree.setClearColor (0x000000, 0); // the default

  this.rendererThree.autoClear = false;

  // makes things look nicer
  this.rendererThree.gammaInput = true;
  this.rendererThree.gammaOutput = true;

  this.rendererThree.physicallyBasedShading = true;

  this.rendererThree.shadowMap.renderReverseSided = false;
  this.rendererThree.shadowMap.renderSingleSided = true;


  this.detectANGLE();

  /**
   * Whether the shadowmap is outdated and shall be recomputed. We actually do not compute shadowmaps each frame,
   * only when the lighting by the sun changes.
   *
   * @type {boolean}
   */
  this.shadowmapOutdated = true;

  /**
   * Array with pixel data of canvas.
   *
   * @type {Uint8Array}
   */
  this.frameBufferSnapshot = new Uint8Array (appMain.getRenderingWindow().windowPixelCountRGBA);

  /**
   * Init 2D rendering context on auxiliary CANVAS element.
   *
   * @type {CanvasRenderingContext2D}
   */
  this.ctx = canvas2d.getContext ("2d");

  /**
   * We can cull manually objects again camera frustum using this.
   *
   * @type {THREE.Frustum}
   */
  this.frustum = new THREE.Frustum();
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// INITIALIZATION
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Initializes the renderer and effects.
 */
Renderer.prototype.init = function()
{
  // (we do this weird thing to fire the setter which shows/hides FPS counter of three.js depending on the settings)
  Preferences.Renderer.SHOW_FPS = Preferences.Renderer.SHOW_FPS;

  if (Preferences.Renderer.NATIVE_GL_RENDERING)
  {
    // similar to situation with Chrome - we cannot write to and read from same texture at the same time when running native gl
    Preferences.Renderer.SOURCE_AND_DESTINATION_TEXTURE_CANNOT_BE_SAME = true;

    // problems with OpenGL drivers: https://www.opengl.org/discussion_boards/showthread.php/176723-ATI-indirect-index-sampler-limitation
    // (the problem is with shader for shadowmaps - shadowmap_fragment.glsl of three.js, we cannot access texture samplers by the variable "i" in loop
    // so modify the problematic shader ang generate the shaderlib again
    THREE.ShaderChunk[ 'shadowmap_fragment'] = SHADOW_MAP_FRAGMENT;
    THREE.my_generateShaderLib();

    console.warn("You are displaying graphics using native WebGL drivers (instead of ANGLE). Shadow mapping will work only for the sun.");
  }

  this.initShadows();

  VisualEffects.init (this.rendererThree);
  VisualEffects.initPostprocessing();
};

/**
 * Inits shadow mapping, depending on the options.
 */
Renderer.prototype.initShadows = function()
{
  this.rendererThree.shadowMap.enabled = Preferences.VisualEffects.SHADOWS;
  this.rendererThree.shadowMap.type = Preferences.VisualEffects.SHADOW_MAP_TYPE;
};

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

/**
 * Detects if the browser is displaying WebGL using native GL driver.
 */
Renderer.prototype.detectANGLE = function()
{
  // it throws context error in Edge, so skip detection and return that we are actually using ANGLE (which I suspect will always be the case, when executed in Edge)
  if (Preferences.Browser.EDGE)
  {
    return true;
  }

  var ctx = this.rendererThree.domElement.getContext ("webgl");

  if (ctx === null)
  {
    ctx = this.domElement.getContext ("experimental-webgl");
  }

  if (ctx)
  {
    var h = ctx.getParameter (ctx.ALIASED_LINE_WIDTH_RANGE);

    // we actually test whether the browser is using ANGLE rendering
    var isANGLE = (h && (h.length === 2) && (h[0] === 1) && (h[1] === 1));

    Preferences.Renderer.NATIVE_GL_RENDERING = !isANGLE;
  }
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// RESIZE CANVAS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Resizes the rendering area.
 *
 * @param {number} width    new width for WebGL content
 * @param {number} height   new height for WebGL content
 */
Renderer.prototype.resize = function (width, height)
{
  appMain.getCurrentEntity().getCamera().getCameraThree().aspect = width / height;
  appMain.getCurrentEntity().getCamera().getCameraThree().updateProjectionMatrix();

  // resize the renderer to the given dimensions, resize the CANVAS element to the size of the browser window (they can differ!)
  this.rendererThree.setSize (width, height, false);
  Gui.setElementSize (canvas, gui.getDisplayWindow().windowWidth, gui.getDisplayWindow().windowHeight);

  // resize also auxiliary 2D canvas!
  canvas2d.width  = width;
  canvas2d.height = height;

  // (resize also CANVAS element of 2D canvas?)

  picking.invalidatePickingMap();
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SHADOW MAP CLEARING
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Manually clears shadow map texture of the sun.
 *
 * This is useful in some Three.js versions, where when we switch app to night mode, the shadows would remain visible
 * e.g. on the ground. This will kill them, so the area will look correct in the night.
 */
Renderer.prototype.clearShadowMap = function()
{
  // see https://github.com/mrdoob/three.js/issues/1055

  //this.rendererThree.shadowMap.autoUpdate = false;

  this.rendererThree.clearTarget (castleScene.sun.shadow.map);
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// RENDERING
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Main render function. Can render different content based on the settings of Preferences.Renderer.RENDERING_CONTENT.
 */
Renderer.prototype.render = function()
{
  switch (Preferences.Renderer.RENDERING_CONTENT)
  {
    ////////////////////////////////////////////////////////
    // NORMAL RENDERING
    ////////////////////////////////////////////////////////

    case Preferences.RenderingContent.STANDARD:
    {
      // note: we access __SHADOWS, not SHADOWS, because SHADOWS has slow getter (because it is linked with UI and stuff)
      if (Preferences.VisualEffects.__SHADOWS && this.shadowmapOutdated)
      {
        this.rendererThree.shadowMap.enabled = true;
      }

      VisualEffects.render();

      if (Preferences.VisualEffects.__SHADOWS && this.shadowmapOutdated)
      {
        this.rendererThree.shadowMap.enabled = false;
        this.shadowmapOutdated = false; // shadow map has been updated
      }
    }
      break;

    // PICKING BUFFER RENDERING (DEV)
    case Preferences.RenderingContent.PICKING_BUFFER:
      this.renderPick();
    break;

    // HEIGHTMAP RENDERING (DEV)
    case Preferences.RenderingContent.HEIGHTMAP:
      this.renderHeightmap();
      break;

    // DEPTH RENDERING (DEV)
    case Preferences.RenderingContent.DEPTH_BUFFER:
      this.renderDepthVisualization();
      break;

    // NO EFFECTS, JUST MAIN SCENE (DEV)
    case Preferences.RenderingContent.NO_EFFECTS:
      renderer.rendererThree.render (castleScene.scene, appMain.getCurrentEntity().camera.cameraThree, undefined, true);
      break;
  }
};

/**
 * Renders the models in scene in their picking colors. Used for picking.
 *
 * IMPORTANT: When you examine the rendered image from this method, you probably won't see anything with your eyes, because
 * there will be colors such (0,0,1) and (0,0,2) and so on. Use an image editor.
 *
 * @param {?THREE.WebGLRenderTarget}    [pickingRenderMapTexture]   if specified, the scene will be rendered into this texture (the usual case; otherwise, it is rendered to the main window - good for DEV)
 */
Renderer.prototype.renderPick = function (pickingRenderMapTexture)
{
  castleScene.scene.overrideMaterial = CustomThreeShaderMaterials["picking"];

  this.rendererThree.render (castleScene.scene, appMain.getCurrentEntity().getCamera().getCameraThree(), pickingRenderMapTexture, true);

  castleScene.scene.overrideMaterial = null;
};

/**
 * Renders depth map of the scene for visualization purposes.
 */
Renderer.prototype.renderDepthVisualization = function ()
{
  castleScene.scene.overrideMaterial = VisualEffects.depthMaterial;

  this.rendererThree.render (castleScene.scene, appMain.getCurrentEntity().getCamera().getCameraThree(), undefined, true);

  castleScene.scene.overrideMaterial = null;
};

/**
 * Renders scene from the orto view (used for heightmap generation).
 *
 * @param {Heightmap}  [heightmap]             heightmap reference
 */
Renderer.prototype.renderHeightmap = function (heightmap)
{
  // make the scene rendered using custom shader for distance encoding (this is our shader, not the Three.js one, although they are similar)
  castleScene.scene.overrideMaterial = CustomThreeShaderMaterials["distance_to_rgba"];
  castleScene.scene.overrideMaterial.side = THREE.DoubleSide;

  this.rendererThree.render (castleScene.scene, Preferences.Heightmap.ORTHO_CAMERA, undefined, true);

  if (Util.isInitialized(heightmap))
  {
    this.saveFrameBuffer (heightmap.valueMap);
  }

  // turn custom shader off
  castleScene.scene.overrideMaterial = null;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// PACKING/UNPACKING DEPTH (FLOAT <-> COLOR)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Packs (encodes) depth (range [0,1]) into the 4D vector, i.e. RGBA color.
 * It uses only RGB portion for encoding, i.e. 24b.
 *
 * This function is JavaScript version of packDepth24() which can be found i.e. in shaders
 * for heightmap (distance_to_rgba.frag) or picking (picking.vert).
 *
 * It is not actually used in the code (because we need to pack in shaders, not in JavaScript), but
 * it is included as well. However, its inverse unpacking function unpackDepth24() is heavily used.
 *
 * @param     {number}       depth   number between [0,1]
 * @returns   {number[]}             4D vector (color) which contains encoded depth in its fields
 */
Renderer.prototype.packDepth24 = function (depth)
{
  var scale  = new THREE.Vector3 (1.0, 256.0, 65536.0);
  var ogb    = new THREE.Vector2 (65536.0 / 16777215.0, 256.0 / 16777215.0);
  var normal = 256.0 / 255.0;

  var unit = new THREE.Vector3 (depth, depth, depth);
  unit.y -= Math.floor (unit.y / ogb.x) * ogb.x;
  unit.z -= Math.floor (unit.z / ogb.y) * ogb.y;

  var color = new THREE.Vector3();
  color.copy (unit);
  color.multiply (scale);

  color.x = color.x - Math.floor (color.x);
  color.y = color.y - Math.floor (color.y);
  color.z = color.z - Math.floor (color.z);

  color.multiplyScalar(normal);

  color.x -= (color.y / 256.0);
  color.y -= (color.z / 256.0);

  return new THREE.Vector4 (Renderer.floatToRGB(color.x), Renderer.floatToRGB(color.y), Renderer.floatToRGB(color.z), Renderer.floatToRGB(1.0)).toArray();
};

/**
 * Unpacks (decodes/reconstructs) depth from range [0,1] which was encoded into 4D RGBA vector.
 * The packing function used only RGB portions, i.e. depth was encoded in 24b.
 *
 * This function is used to unpack colors retrieved from framebuffer e.g. in case of heightmaps or picking.
 *
 * @param     {number[]} rgba_depth    4D vector containing encoded depth in its fields as colors (range [0,255])
 * @returns   {number}                 depth which was encoded in 4D vector, number is in range [0,1]
 */
Renderer.prototype.unpackDepth24 = function (rgba_depth)
{
  // unoptimized version:
  //  var rgb_depth = new THREE.Vector3 (rgba_depth[0], rgba_depth[1], rgba_depth[2]);
  //  var scale = new THREE.Vector3 (65536.0 / 65793.0, 256.0 / 65793.0, 1.0 / 65793.0);
  //  return rgb_depth.dot (scale) / 255; // we get value in 0 to 255, but we want 0 to 1

  // the same as above, more efficient
  return (rgba_depth[0] * (65536.0 / 65793.0) + rgba_depth[1] * (256.0 / 65793.0) + rgba_depth[2] * (1.0 / 65793.0)) / 255;
};

Renderer.prototype.colorId = function(rgb)
{
	var id = 0;
	for(var i = 0; i < 3; i ++){
		id = id * 256 + rgb[i];
	}
	return id;
}

/**
 * Packs (encodes) depth (range [0,1]) into the 4D vector, i.e. RGBA color.
 * It uses all portions for encoding (RGBA), i.e. full 32b.
 *
 * This function is currently not used.
 *
 * @param     {number}       depth   number between [0,1]
 * @returns   {number[]}             4D vector which contains encoded depth in its fields as color
 */
Renderer.prototype.packDepth32 = function (depth)
{
  var bit_shift = new THREE.Vector4 ( 256.0 * 256.0 * 256.0, 256.0 * 256.0, 256.0, 1.0 );
  var bit_mask = new THREE.Vector4 ( 0.0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0 );

  var res = new THREE.Vector4().copy(bit_shift).multiplyScalar(depth).multiplyScalar(255);

  res.x = (res.x % 256) / 255;
  res.y = (res.y % 256) / 255;
  res.z = (res.z % 256) / 255;
  res.w = (res.w % 256) / 255;

  var xxyz_mul_bitMask = new THREE.Vector4(res.x, res.x, res.y, res.z);
  xxyz_mul_bitMask.x *= bit_mask.x;
  xxyz_mul_bitMask.y *= bit_mask.y;
  xxyz_mul_bitMask.z *= bit_mask.z;
  xxyz_mul_bitMask.w *= bit_mask.w;

  res.sub(xxyz_mul_bitMask);

  // values are in float range [0,1], we want them as color values [0,255]
  res.x *= 255;
  res.y *= 255;
  res.z *= 255;
  res.w *= 255;

  return res.toArray(); // we want to return standard array, because unpack function works on standard arrays; modify this behavior if needed
};

/**
 * Unpacks (decodes/reconstructs) depth from range [0,1] which was encoded into 4D RGBA vector.
 * The packing function used all portions of RGBA, i.e. depth was encoded in 32b.
 *
 * This function is currently not used.
 *
 * @param     {number[]} rgba_depth    4D vector containing encoded depth in its fields as colors (range [0,255])
 * @returns   {number}                 depth which was encoded in 4D vector, number is in range [0,1]
 */
Renderer.prototype.unpackDepth32 = function (rgba_depth)
{
  //var bit_shift = new THREE.Vector4(1.0/(256.0*256.0*256.0), 1.0/(256.0*256.0), 1.0/256.0, 1.0);
  //return rgba_depth.dot(bit_shift);

  // more efficient version:
  return (
           rgba_depth[0] * (1.0/(256.0*256.0*256.0)) + rgba_depth[1] * (1.0/(256.0*256.0)) + rgba_depth[2] * (1.0/256.0) +
               ((rgba_depth.length === 4) ? rgba_depth[3] * (1.0) : 0) // if we were given only 3D vector, assume A = 0 (assuming 255 does not work)
         );
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CONVERTING FLOAT <-> RGB <-> GL COLOR
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Converts color in the float range [0,1] to RGB range [0,255].
 *
 * @param   {number} float0to1   floating point number between 0.0 and 1.0
 * @returns {number}             integer number between 0 and 255
 */
Renderer.floatToRGB = function (float0to1)
{
  return Math.min (255, Math.round (float0to1*256));
};

/**
 * Inverse function to floatToRGB(). Converts color in the RGB range [0,255] to float range [0,1].
 *
 * @param   {number}  rgb0to255   integer number between 0 and 255
 * @returns {number}              floating point number between 0.0 and 1.0
 */
Renderer.RGBtoFloat = function (rgb0to255)
{
  return (rgb0to255 / 255);
};

/**
 * Converts vector representing RGB color with integer values (range 0-255 on RGB, A is not taken into account and considered as 255) to the vector with float values (range 0-1).
 *
 * @param   {THREE.Vector4}    rgba  4D RGBA color vector (values between 0-255)
 * @returns {THREE.Vector4}    rgba  modified 4D RGBA color vector (values between 0-1)
 */
Renderer.RGBAToGlColor = function (rgba)
{
  rgba.x = Renderer.RGBtoFloat (rgba.x);
  rgba.y = Renderer.RGBtoFloat (rgba.y);
  rgba.z = Renderer.RGBtoFloat (rgba.z);
  rgba.w = 1.0;

  return rgba;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SCREENSHOTS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Saves the framebuffer into an image and opens it in a new window.
 */
Renderer.prototype.makeScreenshot = function ()
{
  window.open (canvas.toDataURL());
};

/**
 * Makes a screenshot of the auxiliary 2D canvas contents.
 */
Renderer.prototype.makeScreenshot2d = function ()
{
  window.open (canvas2d.toDataURL());
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SAVING THE FRAMEBUFFER
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// "screenshot", i.e. save state of the offscreen WebGL framebuffer

/**
 * Save state of the offscreen WebGL framebuffer into the array from which we can read pixel values.
 *
 * @param whereToSave
 */
Renderer.prototype.saveFrameBuffer = function (whereToSave)
{
  var gl = this.rendererThree.getContext();
  gl.readPixels (0, 0, appMain.getRenderingWindow().windowWidth, appMain.getRenderingWindow().windowHeight, gl.RGBA, gl.UNSIGNED_BYTE, whereToSave);

  // note: beware of the fact that when having window e.g. (width=800, height=600) then the pixel under the mouse at the screen
  // coordinate e.g. (340,250) is actually stored in the framebuffer with coordinate (340,600-250) = (340,350)
  // (WebGL and screen have different coordinate systems)
  // (tip: when making screenshot of canvas, flip it vertically e.g. in IrfanView to convert from screen to WebGL coordinate system -> now the image coordinates under the mouse reported by image viewing program correspond to WebGL coordinates
  // and color of the underlying pixel will be correctly returned by calling Renderer.prototype.readPixelColor() )
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// READING PIXEL COLOR
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Return value of the pixel from given buffer.
 *
 * @param   {*}      buffer
 * @param   {number} width
 * @param   {number} height
 * @param   {number} x
 * @param   {number} y
 * @returns {number}
 */
Renderer.getPixelValue = function (buffer, width, height, x, y)
{
  return (y * 4 * width + x * 4);
};

/**
 * Checks if the pixel is defined, based on its color.
 *
 * @param   {THREE.Vector3|THREE.Vector4} pixel
 * @returns {boolean}
 */
Renderer.isPixelDefined = function (pixel)
{
  // if pixel is RGBA, do not check last value (alpha)
  for (var i = 0, len = Math.min (pixel.length, 3); i < len; i++)
  {
    if (pixel[i] !== 0)
    {
      return true;
    }
  }

  return false;
};

/**
 * Reads color of the pixel from the buffer at the given position.
 *
 * @param   {Uint8Array}  buffer    buffer (array) with pixels
 * @param   {number}      width     width of the buffer (width of framebuffer from which we have this values)
 * @param   {number}      height    height of the buffer
 * @param   {number}      x
 * @param   {number}      y
 * @returns {number[]}              color of the pixel as regular array (not THREE vector!)
 */
Renderer.readPixelColor = function (buffer, width, height, x, y)
{
  if (x >= width || y >= height || x < 0 || y < 0)
  {
    // return color of background - nothing picked, since we are out of canvas
    return Preferences.Picking.NOTHING_PICKED_COLOR_RGBA;
  }

  if (!buffer)
  {
    console.error("ERROR: Framebuffer was not captured?");
  }

  //var firstAddress = (y - 1) * state.windowWidth * 4 + x * 4;
  var firstAddress = y * 4 * width + x * 4;


  var out = [
    buffer[firstAddress],
    buffer[firstAddress + 1],
    buffer[firstAddress + 2],
    buffer[firstAddress + 3]
];

  return out;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// LOADING TEXTURE FROM FILE INTO THE ARRAY
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// |Note: PIXEL STORAGE FORMAT
// Take following image for example (letters are pixels):
//
// |A|B|C|D|
// |E|F|G|H|
// |I|J|K|L|
//
// order of pixel storage in the image data array will be: I,J,K,L,E,F,G,H,A,B,C,D

/**
 * Retrieves color of the image pixels.
 * Output is huge flat array of pixels, which will be stored in the outObject[outPropertyName] via reflection.
 * Each pixel spans 4 consecutive array cells (color portions R,G,B,A).
 * The first stored pixel is at the bottom-left corner of the image. Pixels are stored in rows, from left to right.
 * First stored row is actually last image row (n-th row), second stored row is (n-1th row), ... last stored row is first row.
 * So the last stored pixel is at the top-right corner of the image.
 *
 * @param {Image}   image             input image
 * @param {*}       outObject         into which object will be pixel data saved
 * @param {String}  outPropertyName   into which property of the outObject will be pixel data saved
 */
Renderer.prototype.getImageData = function (image, outObject, outPropertyName)
{
  // (note: don't do ctx.width = imageObj.width etc., will draw in incorrect size)
  Gui.setCanvas2dSize (image.width, image.height);

  // SCALING

  this.ctx.save();

  // flip vertically to go into WebGL coordinate system
  this.ctx.scale (1, -1);


  this.ctx.clearRect (0, 0, canvas2d.width, canvas2d.height);

  // start at (0, -height) ... that is now the bottom-left corner
  this.ctx.drawImage (image, 0, -image.height); // SCALING

  //ctx.drawImage(image, 0, 0); // standard draw without scaling

  outObject[outPropertyName] = this.ctx.getImageData(0, 0, canvas2d.width, canvas2d.height).data;

  // go back to standard screen coordinate system
  this.ctx.restore();
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// GETTERS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Returns Three.js renderer reference.
 *
 * @returns {THREE.WebGLRenderer}
 */
Renderer.prototype.getRendererThree = function()
{
  return this.rendererThree;
};
