"use strict";

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

/**
 * This (singleton) class configures rendering passes based on effects which are turned on
 * and is responsible for what is rendered on the screen.
 *
 * @author smrceant (Antonin Smrcek)
 */
var VisualEffects = new function()
{
  /**
   * For composing several rendering/post-processing passes.
   *
   * @type {THREE.EffectComposer}
   */
  this.effectComposer = null;

  /**
   * For composing post-processing passes for Lens Flare effect.
   *
   * @type {THREE.EffectComposer}
   */
  this.effectComposerLens = null;

  /**
   * Rendering/post-processing passes.
   *
   * @type {THREE.RenderPass|THREE.ShaderPass}
   */
  this.passes = {};

  /**
   *
   * @type {THREE.WebGLRenderTarget}
   */
  this.renderTarget = null;

  /**
   *
   * @type {THREE.WebGLRenderTarget}
   */
  this.normalSceneRenderTarget = null;

  /**
   *
   * @type {THREE.WebGLRenderTarget}
   */
  this.lensRenderTarget = null;

  /**
   *
   * @type {THREE.WebGLRenderTarget}
   */
  this.pickableModelsVisualizationTarget = null;
}();

/**
 * Creates render targets and effect composers for post-processing effects.
 *
 * @param {THREE.WebGLRenderer} renderer
 * @type  {function}
 */
VisualEffects.init = function (renderer)
{
  var _this = this;

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

  // target where scene without any effects will be rendered (at the start of each render())
  this.normalSceneRenderTarget = new THREE.WebGLRenderTarget (window.innerWidth, window.innerHeight,
      {
        minFilter:     THREE.LinearFilter,
        magFilter:     THREE.LinearFilter,
        format:        THREE.RGBAFormat
      });
  appMain.getRenderingWindow().updateObjectAfterResize (this.normalSceneRenderTarget.setSize.bind (this.normalSceneRenderTarget));

  /////////////////////////////////////////////////////////////////////////
  // AUXILIARY RENDER TARGETS
  /////////////////////////////////////////////////////////////////////////

  // (do not use other shaders, e.g. new Three.js depth shader or something like that - won't work!)
  var depthShaderRGBA = THREE.ShaderLib["depthRGBA"];

  // https://github.com/mrdoob/three.js/blob/master/examples/webgl_postprocessing_ssao.html
  // creating depth map of a scene - for SSAO
  this.depthMaterial = new THREE.ShaderMaterial(
      {
        fragmentShader: depthShaderRGBA.fragmentShader,
        vertexShader: depthShaderRGBA.vertexShader,
        uniforms: THREE.UniformsUtils.clone (depthShaderRGBA.uniforms),
        blending: THREE.NoBlending
      });

  //this.depthMaterial = new THREE.MeshDepthMaterial();
  //this.depthMaterial.depthPacking = THREE.RGBADepthPacking;
  //this.depthMaterial.blending = THREE.NoBlending;

  // creating depth map of a scene - for SSAO
  this.depthRenderTarget = new THREE.WebGLRenderTarget (window.innerWidth, window.innerHeight,
      {
        minFilter: THREE.LinearFilter,
        magFilter: THREE.LinearFilter
      });
  appMain.getRenderingWindow().updateObjectAfterResize (this.depthRenderTarget.setSize.bind (this.depthRenderTarget));

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

  /////////////////////////////////////////////////////////////////////////
  // TARGET WHERE THE MAIN IMAGE (INCL. POST-PROC. EFF. WILL BE RENDERED)
  /////////////////////////////////////////////////////////////////////////

  // create a custom render target with a stencil buffer
  // the stencil buffer allows for masking to take place
  this.renderTarget = new THREE.WebGLRenderTarget (window.innerWidth, window.innerHeight,
      {
        minFilter:     THREE.LinearFilter,
        magFilter:     THREE.LinearFilter,
        format:        THREE.RGBAFormat
      });
  appMain.getRenderingWindow().updateObjectAfterResize (this.renderTarget.setSize.bind (this.renderTarget));

  this.renderTarget2 = new THREE.WebGLRenderTarget (window.innerWidth, window.innerHeight,
      {
        minFilter:     THREE.LinearFilter,
        magFilter:     THREE.LinearFilter,
        format:        THREE.RGBAFormat
      });
  appMain.getRenderingWindow().updateObjectAfterResize (this.renderTarget2.setSize.bind (this.renderTarget2));

  //////////////////////////////////////////////////////////////
  // VISUALIZATION OF PICKABLE MODELS
  //////////////////////////////////////////////////////////////

  this.pickableModelsVisualizationTarget = new THREE.WebGLRenderTarget (window.innerWidth, window.innerHeight,
      {
        minFilter: THREE.LinearFilter,
        magFilter: THREE.NearestFilter,
        format: THREE.RGBAFormat
      });
  appMain.getRenderingWindow().updateObjectAfterResize (this.pickableModelsVisualizationTarget.setSize.bind (this.pickableModelsVisualizationTarget));

  //////////////////////////////////////////////////////////////
  // (EFFECT COMPOSER)
  //////////////////////////////////////////////////////////////

  if (Preferences.Renderer.SOURCE_AND_DESTINATION_TEXTURE_CANNOT_BE_SAME)
  {
    this.effectComposer = new THREE.EffectComposer (renderer, this.renderTarget2);
  }
  else
  {
    this.effectComposer = new THREE.EffectComposer (renderer, this.renderTarget);
  }
  appMain.getRenderingWindow().updateObjectAfterResize (this.effectComposer.setSize.bind (this.effectComposer));

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

  //////////////////////////////////////////////////////////////
  // LENS FLARE
  //////////////////////////////////////////////////////////////

  this.lensRenderTarget	= new THREE.WebGLRenderTarget (this.renderTarget.width/2, this.renderTarget.height/2, {
    minFilter	: THREE.LinearFilter,
    magFilter	: THREE.LinearFilter,
    format		: THREE.RGBFormat
  });
  appMain.getRenderingWindow().addResizingCallback (function()
  {
    _this.lensRenderTarget.setSize (_this.renderTarget.width / 2, _this.renderTarget.height / 2);
  });

  //////////////////////////////////////////////////////////////
  // (EFFECT COMPOSER FOR LENS FLARE)
  //////////////////////////////////////////////////////////////

  if (Preferences.Renderer.SOURCE_AND_DESTINATION_TEXTURE_CANNOT_BE_SAME)
  {
    this.effectComposerLens = new THREE.EffectComposer (renderer, this.renderTarget);
  }
  else
  {
    this.effectComposerLens = new THREE.EffectComposer (renderer, this.lensRenderTarget);
  }
  appMain.getRenderingWindow().updateObjectAfterResize (this.effectComposerLens.setSize.bind (this.effectComposerLens));

}.bind(VisualEffects);

/**
 * Main rendering function which defines how the rendering process looks.
 */
VisualEffects.render = function()
{
  ///////////////////////////////////////////////////////////////////
  // INITIAL RENDER OF SCENE AND SKY TO TEXTURE
  ///////////////////////////////////////////////////////////////////

  renderer.rendererThree.render (castleScene.sceneSky, appMain.getCurrentEntity().camera.cameraThree, this.normalSceneRenderTarget, true);
  renderer.rendererThree.render (castleScene.scene, appMain.getCurrentEntity().camera.cameraThree, this.normalSceneRenderTarget);

  ///////////////////////////////////////////////////////////////////
  // VISUAL EFFECTS RENDER
  ///////////////////////////////////////////////////////////////////

  if (Preferences.VisualEffects.POST_PROCESSING.SSAO)
  {
    // we need depth map of the scene

    // (three.js, THREE.WebGlRenderer)
    // we had to add a line above the "renderObjects( transparentObjects, camera, lights, fog, overrideMaterial );", check the three.js
    // basically when this flag is on, we throw out all sprites from the render list (this prevents sprite being rendered with the depth RGBA shader, which is what we want - otherwise, quad polygon of sprite is rendered into the depth map of ambient occlusion and that is very bad)
    Preferences.Renderer.doNotRenderSpritesWithOverrideMaterial = true;

    // also hide info cubes
    castleScene.hideInfoCubes();

    castleScene.scene.overrideMaterial = this.depthMaterial;
    renderer.rendererThree.render (castleScene.scene, appMain.getCurrentEntity().camera.cameraThree, this.depthRenderTarget, true);
    castleScene.scene.overrideMaterial = null;

    // make them visible again only if the are allowed to be visible
    if (Preferences.Picking.INFO_CUBES)
    {
      castleScene.showInfoCubes();
    }

    Preferences.Renderer.doNotRenderSpritesWithOverrideMaterial = false;
  }

  // render pick draw, we will later blend it with the other passes
  if (Preferences.VisualEffects.POST_PROCESSING.PICKING_VISUALIZATION)
  {
    renderer.renderPick (this.pickableModelsVisualizationTarget);
  }

  // we will now try to optimize lens flare effect - we will detect if the sun sphere - which is the only
  // reasonable object in our app that produces flares - is in frustum and if not, we simply do not launch the flare passes)
  if (Preferences.VisualEffects.POST_PROCESSING.LENS_FLARE)
  {
    // if sun was in frustum last frame
    if (VisualEffects.Sky.sunSphereInFrustum)
    {
      // (note: modification in Three.js must have been made - we have to made the frustum publicly accessible)
      // (note2: we also test if the sun is currently visible, otherwise the shaders would be executed even if the sun is "under the terrain"; and watch out,
      //         we check visibility of light, not of mesh, because we hide the light, not the mesh)
      VisualEffects.Sky.sunSphereInFrustum = castleScene.sun.visible && renderer.rendererThree.frustum.intersectsObject (VisualEffects.Sky.sunSphere);

      // last frame the sun was in frustum and now it is not - we have to update rendering passes
      if (!VisualEffects.Sky.sunSphereInFrustum)
      {
        this.updatePostprocessingPasses();
      }
    }
    // sun was not in frustum last frame
    else
    {
      VisualEffects.Sky.sunSphereInFrustum = castleScene.sun.visible && renderer.rendererThree.frustum.intersectsObject (VisualEffects.Sky.sunSphere);

      // last frame the sun was not in frustum and now it is - we have to update rendering passes
      if (VisualEffects.Sky.sunSphereInFrustum)
      {
        this.updatePostprocessingPasses();
      }

    }
  }

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

  // Firefox - render lens flare before final render
  if (!Preferences.Renderer.SOURCE_AND_DESTINATION_TEXTURE_CANNOT_BE_SAME)
  {
    if (Preferences.VisualEffects.POST_PROCESSING.LENS_FLARE && VisualEffects.Sky.sunSphereInFrustum)
    {
      VisualEffects.passes["lensFlareBlend"].update (appMain.getCurrentEntity().camera.getCameraThree());

      this.effectComposerLens.render();
    }
  }

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

  ///////////////////////////////////////////////////////////////////
  // FINAL RENDER OF ALL TOGETHER
  ///////////////////////////////////////////////////////////////////

  this.effectComposer.render();

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

  // chrome and native gl - render lens flare after final render
  if (Preferences.Renderer.SOURCE_AND_DESTINATION_TEXTURE_CANNOT_BE_SAME)
  {
    if (Preferences.VisualEffects.POST_PROCESSING.LENS_FLARE && VisualEffects.Sky.sunSphereInFrustum)
    {
      VisualEffects.passes["lensFlareBlend"].update (appMain.getCurrentEntity().camera.getCameraThree());

      this.effectComposerLens.render();
    }
  }

}.bind(VisualEffects);

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// INITIALIZATION OF POST-PROCESSING EFFECTS AND RENDER PASSES
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

VisualEffects.initPostprocessing = function()
{
  var _this = this;

  // we always render the scene first into the texture, because the scene can be used by different post-processing algorithms like that
  //   (the things is, it seems that during the execution of EffectComposer, the renderTarget is not filled with data yet, so some algorithms in effects chain cannot work properly - e.g. lens flare which must have renderTarget filled with data)
  //   (I'm sure I'm missing something/doing something wrong/not doing something which should be, but I have no idea..)

  ///////////////////////////////////////////////////////////////////
  // SCREEN SPACE AMBIENT OCCLUSION
  ///////////////////////////////////////////////////////////////////

  // source of SSAO implementation: http://threejs.org/examples/webgl_postprocessing_ssao.html
  // other good resources:
    // http://alteredqualia.com/three/examples/webgl_postprocessing_ssao.html
    // http://jsfiddle.net/xqLdz/3/, http://stackoverflow.com/questions/15275986/rendering-multiple-scenes-with-only-1-using-ssao-three-js

  this.passes["SSAO"] = new THREE.ShaderPass (THREE.SSAOShader);
  //this.passes["SSAO"].uniforms["tDiffuse"].value will be set by ShaderPass
  this.passes["SSAO"].uniforms["tDepth"].value       = VisualEffects.depthRenderTarget;
  this.passes["SSAO"].uniforms["size"].value.set (appMain.getRenderingWindow().windowWidth, window.innerHeight);
  appMain.getRenderingWindow().updateObjectAfterResize (this.passes["SSAO"].uniforms["size"].value.set.bind (this.passes["SSAO"].uniforms["size"]));

  // (improvement: add callback to the camera when its near or far changes; it probably won't happen, but who knows?)
  this.passes["SSAO"].uniforms["cameraNear"].value   = appMain.getCurrentEntity().camera.getCameraThree().near;
  this.passes["SSAO"].uniforms["cameraFar"].value    = appMain.getCurrentEntity().camera.getCameraThree().far;
  this.passes["SSAO"].uniforms["aoClamp"].value      = Preferences.VisualEffects.POST_PROCESSING.__SSAO_AO_CLAMP;
  this.passes["SSAO"].uniforms["lumInfluence"].value = Preferences.VisualEffects.POST_PROCESSING.__SSAO_LUM_INFLUENCE;

  ///////////////////////////////////////////////////////////////////
  // FAST APPROXIMATE ANTI-ALIASING
  ///////////////////////////////////////////////////////////////////

  // source: from three.js library of shaders

  this.passes["FXAA"] = new THREE.ShaderPass( THREE.FXAAShader );
  this.passes["FXAA"].uniforms["resolution"].value.set (1 / window.innerWidth, 1 / window.innerHeight);
  appMain.getRenderingWindow().addResizingCallback(function()
  {
    _this.passes["FXAA"].uniforms["resolution"].value.set (1 / appMain.getRenderingWindow().windowWidth, 1 / appMain.getRenderingWindow().windowHeight);
  });
  ///////////////////////////////////////////////////////////////////
  // COPYING RENDERED IMAGES IN TEXTURES TO THE SCREEN
  ///////////////////////////////////////////////////////////////////

  // if SSAO or FXAA pass is the last one, everything is ok and we get a picture, otherwise this pass must be last in order to render something!
  this.passes["output"] = new THREE.ShaderPass (THREE.CopyShader);

  //////////////////////////////////////////////////////////////////////////
  // COMPOSITING A TEXTURE WITH SOME EFFECT AND SO FAR POST-PROCESSED SCENE
  //////////////////////////////////////////////////////////////////////////

  /**
   * Utility function for creating compositing pass where one render target is blended into another.
   * Used for post-processing when we need to combine two textures together.
   *
   * @type    {function}
   * @returns {THREE.ShaderPass}
   */
  var createCompositingPassOfTwoTargets = function (renderTarget, targetThatWillBeBlendedWithRenderTarget)
  {
    var compositingPass = new THREE.ShaderPass (THREE.AdditiveShader, renderTarget);
    compositingPass.needsSwap = true;
    compositingPass.uniforms["tDiffuse"].value = renderTarget;
    compositingPass.uniforms["tAdd"].value     = targetThatWillBeBlendedWithRenderTarget;

    return compositingPass;
  }.bind (this);

  // compositing image of  normal scene / pickable models / lens flare with so far post-processed scene
  this.passes["normalSceneWithRenderTargetComposite"]    = createCompositingPassOfTwoTargets (this.renderTarget, this.normalSceneRenderTarget);

  if (Preferences.Renderer.SOURCE_AND_DESTINATION_TEXTURE_CANNOT_BE_SAME)
  {
    this.passes["pickableModelsWithRenderTargetComposite"] = createCompositingPassOfTwoTargets (this.renderTarget2, this.pickableModelsVisualizationTarget);
  }
  else
  {
    this.passes["pickableModelsWithRenderTargetComposite"] = createCompositingPassOfTwoTargets (this.renderTarget, this.pickableModelsVisualizationTarget);
  }

  this.passes["lensFlareWithRenderTargetComposite"]      = createCompositingPassOfTwoTargets (this.renderTarget, this.lensRenderTarget);

  //////////////////////////////////////////////////////////////////////////
  // LENS FLARE
  //////////////////////////////////////////////////////////////////////////

  // source: https://github.com/jeromeetienne/threex.sslensflare

  // lens flare is generated from the normal scene (we need the rendered texture with normal scene)
  this.passes["lensFlare"]	= new THREEx.SsLensFlare.Passes (renderer.getRendererThree(), this.normalSceneRenderTarget, this.lensRenderTarget);

  // special blend which make lensStar rotate based on camera rotation
  this.passes["lensFlareBlend"]	= new THREEx.SsLensFlare.BlendPass (this.lensRenderTarget);

  ///////////////////////////////////////////////////////////////////
  // FILLING EFFECT COMPOSERS - ADDING ALL PASSES TOGETHER
  ///////////////////////////////////////////////////////////////////

  // MAIN EFFECT COMPOSER
    // we just add ALL created passes - they are enabled/disabled by updatePostprocessingPasses()
  this.effectComposer.addPass (this.passes["normalSceneWithRenderTargetComposite"]);
  this.effectComposer.addPass (this.passes["SSAO"]);
  this.effectComposer.addPass (this.passes["FXAA"]);
  this.effectComposer.addPass (this.passes["output"]);
  this.effectComposer.addPass (this.passes["pickableModelsWithRenderTargetComposite"]);
  this.effectComposer.addPass (this.passes["lensFlareWithRenderTargetComposite"]);

  // LENS FLARE COMPOSER
  this.passes["lensFlare"].addPassesTo (this.effectComposerLens);
  this.passes["lensFlareBlend"].addPassesTo   (this.effectComposerLens);

  // enable/disable passes based on settings
  this.updatePostprocessingPasses();
}.bind(VisualEffects);

/**
 * Updates configuration of the rendering passes, i.e. what passes are enabled, what pass
 * is rendered to the screen and so on.
 */
VisualEffects.updatePostprocessingPasses = function()
{
  this.passes["SSAO"].enabled = Preferences.VisualEffects.POST_PROCESSING.SSAO;
  this.passes["FXAA"].enabled = Preferences.VisualEffects.POST_PROCESSING.FXAA;

  // if SSAO or FXAA passes are active, we need copy shader after them; also we need it when no composite pass is present to actually output something on the screen

  if (Preferences.Renderer.SOURCE_AND_DESTINATION_TEXTURE_CANNOT_BE_SAME)
  {
    this.passes["output"].enabled = true;
  }
  else
  {
    this.passes["output"].enabled = Preferences.VisualEffects.POST_PROCESSING.SSAO || Preferences.VisualEffects.POST_PROCESSING.FXAA || (!Preferences.VisualEffects.POST_PROCESSING.PICKING_VISUALIZATION && !(Preferences.VisualEffects.POST_PROCESSING.LENS_FLARE && VisualEffects.Sky.sunSphereInFrustum));
  }

  // if active, then these passes can be last, since they output an image
  this.passes["pickableModelsWithRenderTargetComposite"].enabled = Preferences.VisualEffects.POST_PROCESSING.PICKING_VISUALIZATION;
  this.passes["lensFlareWithRenderTargetComposite"].enabled      = (Preferences.VisualEffects.POST_PROCESSING.LENS_FLARE && VisualEffects.Sky.sunSphereInFrustum) && !Preferences.Renderer.SOURCE_AND_DESTINATION_TEXTURE_CANNOT_BE_SAME; // if we cannot render to the same texture, we do not composite but render over

  // note: we do nothing for effectComposerLens, no need to enable/disable individual passes in it - if it is active, all passes shall be enabled

  ///////////////////////////////////////////////////////////////////
  // MAKING THE LAST PASS RENDER TO THE SCREEN
  ///////////////////////////////////////////////////////////////////

  // (note: this is actually not needed when the last pass is composite pass, but it is safer to do that anyway)

  // make the last enabled pass render to screen and other not
  for (var i = this.effectComposer.passes.length- 1, lastFound = false; i >= 0; i--)
  {
    if (lastFound)
    {
      this.effectComposer.passes[i].renderToScreen = false;
      continue;
    }

    if (this.effectComposer.passes[i].enabled)
    {
      lastFound = true;
      this.effectComposer.passes[i].renderToScreen = true;
    }
  }

  // note: we do nothing for effectComposerLens

}.bind(VisualEffects);

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

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Sun and Sky
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Sun and sky simulation "singleton" class. Can be accessed using just "VisualEffects.Sky".
 * Uses implementation from three.js examples, "sky shader".
 */
VisualEffects.Sky = new function()
{
  var _this = this;

  /**
   * Is the sky shader active?
   * @type {boolean}
   */
  this.active = this._active = false;

  /**
   * When was the day/night simulation last updated.
   *
   * @type {number}
   */
  this.lastUpdateFrame = 0;

  /**
   *
   * @type {THREE.Object3D}
   */
  this.entireSky = new THREE.Object3D();
  this.entireSky.visible = false;
  castleScene.sceneSky.add (this.entireSky);

  /**
   * Shader for day/night visual effect.
   *
   * @type {THREE.Sky}
   */
  this.sky = new THREE.Sky();
  this.entireSky.add (this.sky.mesh);

  /**
   * Mesh for sun on the sky.
   *
   * @type {THREE.Mesh}
   */
  this.sunSphere = new THREE.Mesh
  (
      new THREE.SphereBufferGeometry (Preferences.Lights.SUN_MESH_SIZE, Preferences.Lights.SUN_MESH_SEGMENTS.x, Preferences.Lights.SUN_MESH_SEGMENTS.y),
      new THREE.MeshBasicMaterial({ color: Preferences.Lights.SUN_MESH_COLOR.getHex() })
  );
  this.sunSphere.visible = false;
  this.entireSky.add (this.sunSphere);

  /**
   * TRUE if the sunSphere mesh is in the camera frustum. Used for optimalization.
   *
   * @type {boolean}
   */
  this.sunSphereInFrustum = false;

  this.sunShaderParameters = Preferences.Lights.SUN_SHADER_PARAMETERS;

  if (Preferences.Application.isDevMode() && Preferences.Lights.SHOW_GUI_FOR_SUN)
  {
    var updateSkyClosure = function()
    {
      _this.updateSky();
    };
  }
}();

VisualEffects.Sky.doNight = function (castleScene, renderer)
{
    // (just night)
    castleScene.ambientLight.color.copy (Preferences.Lights.SCENE_TERRAIN_AMBIENT_LIGHT_AT_NIGHT.color);

    // this is the first frame that the sun is hidden - hide it
    if (castleScene.sun.visible)
    {
      castleScene.sun.visible = false;

      renderer.clearShadowMap();
    }

    if (Preferences.VisualEffects.NIGHT_LIGHTS)
    {
      castleScene.turnNightLights (true);
    }
}

VisualEffects.Sky.doDay = function(castleScene, renderer, zenith)
{
    // we want these values: 06:00 = 0.0 (no light), 12:00 = 1.0 (full light), 18:00 = 0.0 (no light) ... for example, at 09:00 and 15:00 we want 0.5 (half light)
    var whereIsTimeBetween6And18;

    if (Preferences.Lights.TIME_OF_THE_DAY < 12) // between 6 and 12
    {
      whereIsTimeBetween6And18 = (Preferences.Lights.TIME_OF_THE_DAY - 6) / 6;
    }
    else // between 12 and 18
    {
      whereIsTimeBetween6And18 = 1.0 - ((Preferences.Lights.TIME_OF_THE_DAY - 12) / 6);
    }

    // (just to be sure)
    whereIsTimeBetween6And18 = Util.clamp (whereIsTimeBetween6And18, 0, 1);

    castleScene.sun.color.setRGB (whereIsTimeBetween6And18, whereIsTimeBetween6And18, whereIsTimeBetween6And18);

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

    // dusk <-> night
    if (zenith > Preferences.Lights.STARTING_DUSK_OR_DAWN_ZENITH_ANGLE)
    {
      // dusk -> night
      castleScene.ambientLight.color.copy (Preferences.Lights.SCENE_TERRAIN_AMBIENT_LIGHT_AT_DUSK_OR_DAWN.color);
      castleScene.ambientLight.color.lerp (Preferences.Lights.SCENE_TERRAIN_AMBIENT_LIGHT_AT_NIGHT.color, (zenith - Preferences.Lights.STARTING_DUSK_OR_DAWN_ZENITH_ANGLE) / (Preferences.Lights.STARTING_NIGHT_ZENITH_ANGLE - Preferences.Lights.STARTING_DUSK_OR_DAWN_ZENITH_ANGLE));
    }
    else
    {
      // day <-> dusk
      castleScene.ambientLight.color.copy (Preferences.Lights.SCENE_TERRAIN_AMBIENT_LIGHT_AT_DAY.color);
      castleScene.ambientLight.color.lerp (Preferences.Lights.SCENE_TERRAIN_AMBIENT_LIGHT_AT_DUSK_OR_DAWN.color, (zenith - Preferences.Lights.STARTING_DAY_ZENITH_ANGLE) / (Preferences.Lights.STARTING_DUSK_OR_DAWN_ZENITH_ANGLE - Preferences.Lights.STARTING_DAY_ZENITH_ANGLE));
    }

    // was the sun invisible in the last frame? i.e. this is the first frame of the day
    if (!castleScene.sun.visible)
    {
      castleScene.sun.visible = true;
    }

    castleScene.turnNightLights (false);
}

/**
 * Updates state of sky and sun simulation, i.e. move the sun depending on the value of inclination and azimuth.
 */
VisualEffects.Sky.updateSky = function()
{
  this.sunSphere.visible = Preferences.Lights.SUN_DISPLAY_BIG_MESH;

  if (Preferences.Application.isDevMode() && Preferences.Lights.SHOW_GUI_FOR_SUN)
  {
    this.sky.uniforms.turbidity.value = this.sunShaderParameters.turbidity;
    this.sky.uniforms.reileigh.value = this.sunShaderParameters.reileigh;
    this.sky.uniforms.luminance.value = this.sunShaderParameters.luminance;
    this.sky.uniforms.mieCoefficient.value = this.sunShaderParameters.mieCoefficient;
    this.sky.uniforms.mieDirectionalG.value = this.sunShaderParameters.mieDirectionalG;
  }

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

  // mapping time -> azimuth
  Preferences.Lights.SUN_AZIMUTH = (Preferences.Lights.TIME_OF_THE_DAY / 24);

  // we want to have start (and end value) of azimuth at 18:00, so 06:00 will have azimuth of 0.5 (dusk)
  Preferences.Lights.SUN_AZIMUTH = (Preferences.Lights.SUN_AZIMUTH + 6/24) % 1;

  // minimum azimuth which we allow is 0.04 (values below look bad) and maximum is 1.00, thus range is 0.96
  Preferences.Lights.SUN_AZIMUTH = 0.04 + Preferences.Lights.SUN_AZIMUTH * 0.96;

  var theta =     Math.PI * (Preferences.Lights.SUN_INCLINATION - 0.5 ); // how high is sun set
  var phi   = 2 * (Math.PI + 2 * Preferences.Lights.SUN_AZIMUTH_ANGLE_CORRECTION) * (Preferences.Lights.SUN_AZIMUTH - 0.5) - Preferences.Lights.SUN_AZIMUTH_ANGLE_CORRECTION; // horizontal moving range

  this.sunSphere.position.x = Preferences.Lights.SUN_DISTANCE * Math.cos (phi);
  this.sunSphere.position.y = Preferences.Lights.SUN_DISTANCE * Math.sin (phi) * Math.sin (theta);
  this.sunSphere.position.z = Preferences.Lights.SUN_DISTANCE * Math.sin (phi) * Math.cos (theta);

  // update sun position in the shader
  this.sky.uniforms.sunPosition.value.copy (this.sunSphere.position);

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

  castleScene.sun.position.copy (this.sunSphere.position);

  // sun shall be hidden if the angle is greater than 90 deg
  var zenith = castleScene.sun.position.angleTo (Preferences.Lights.UP_VECTOR);

  if (zenith >= Preferences.Lights.STARTING_NIGHT_ZENITH_ANGLE - 0.015)  // necessary to apply a little epsilon, or sometimes the sun would be still visible even if it is like 99.99% night
  {
      this.doNight(castleScene, renderer);
  }
  // sun is above the horizon and shall be visible
  else
  {
      this.doDay(castleScene, renderer, zenith);
  }

  castleScene.sun.shadow.camera.updateProjectionMatrix();
  renderer.shadowmapOutdated = true;

  // visible sun above horizon = to ground angle must be between 0 to 90 degrees

  VisualEffects.Sky.lastUpdateFrame = clock.elapsedFrames;
}.bind (VisualEffects.Sky);

/**
 * Updates our time simulation (recomputes tiem of the day based on last frame delta).
 */
VisualEffects.Sky.updateTimeSimulation = function()
{
  // update only if the simulation is active and the menu is not currently opened

  if (Preferences.Lights.TIME_SIMULATION && (gui.mainMenu && !gui.mainMenu.visible))
  {
    var now = Preferences.Lights.TIME_OF_THE_DAY;
    var delta = appMain.getLastFrameDeltaRatio();
    var increment = Preferences.Lights.TIME_SIMULATION_SPEED_INC * 0.0002777777; // INC / 3600
    Preferences.Lights.TIME_OF_THE_DAY = (now + (increment * delta)) % 24;

    ///////////////////////////////
    // UPDATING SKY
    ///////////////////////////////

    // (note: I wanted to do call this in VisualEffects.render(), but somehow it didn't work properly)

    if (clock.elapsedFrames - VisualEffects.Sky.lastUpdateFrame > Preferences.Lights.SUN_UPDATE_INTERVAL_FRAMES)
    {
      VisualEffects.Sky.updateSky();
    }

    ///////////////////////////////
  }
}.bind (VisualEffects.Sky);

//////////////////////////////////////////////////////////////
// ACTIVATION/DEACTIVATION
//////////////////////////////////////////////////////////////

/**
 * Changes sun/sky shader active state.
 *
 * @param {boolean}   state
 */
VisualEffects.Sky.setState = function (state)
{
  if (state)
  {
    this.activate();
  }
  else
  {
    this.deactivate();
  }
}.bind(VisualEffects.Sky);

/**
 * Activates the sun shader.
 */
VisualEffects.Sky.activate = function()
{
  this._active = true;
  this.entireSky.visible = true;

  this.updateSky();

  if (appMain.getCurrentEntity().camera.far !== Preferences.Lights.SUN_SHADOW_CAMERA_FAR)
  {
    if (Preferences.Application.isDevMode())
    {
      console.log ("Sky shader activated - camera far set to " + Preferences.Lights.SUN_SHADOW_CAMERA_FAR);
    }

    appMain.getCurrentEntity().camera.cameraThree.far = Preferences.Lights.SUN_SHADOW_CAMERA_FAR;
    appMain.getCurrentEntity().camera.cameraThree.updateProjectionMatrix();
  }
}.bind(VisualEffects.Sky);

/**
 * Deactivates the sun shader.
 */
VisualEffects.Sky.deactivate = function()
{
  this._active = false;
  this.entireSky.visible = false;

  // restore default position for sun when the shader is turned off
  castleScene.sun.position.copy (Preferences.Lights.SCENE_DEFAULT_DIRECTIONAL_LIGHT_POSITION);
}.bind(VisualEffects.Sky);

Object.defineProperty (VisualEffects.Sky, "active",
    {
      set: VisualEffects.Sky.setState,
      get: function()
      {
        return this._active;
      }
    });

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