'use strict';

/**
 ***************************************************************************************
 * Name:        Visualization of Prague Castle
 * What is it:  Diploma Thesis
 * Author:      Antonin Smrcek (smrceant@fel.cvut.cz, tonda.smrcek@gmail.com, antonin.smrcek@gmail.com)
 * WWW:         https://sites.google.com/site/praguecastlevisualization/
 * Online:      http://leyfi.felk.cvut.cz/prague-castle-3d/
 * Finish date: January 2016
 ***************************************************************************************
 */

/**
 * Version History:
 *
 * January 2016
 * - initial version
 *
 * January 2017
 * - fixed shadow behavior
 * - fixed problems with rendering on new versions of Firefox
 * - fixed sprite rendering problems
 *
 * - improved collision detection
 * - improved tourist guide articles
 *
 * - added a few elements (water, distant terrain, Petrin Tower)
 *
 * - updated to newer version of Three.js
 */

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// GLOBAL VARIABLES
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// (I don't consider them as such evil)
  // UPDATE: Looking back at it, it could have been done in a better way... oh well...

/**
 * Three.js FPS stats.
 *
 * @type {Stats}
 */
var statsThree = new Stats();

/**
 * CANVAS element used for WebGL display.
 *
 * @type {HTMLCanvasElement}
 */
var canvas;

/**
 * Auxiliary CANVAS element, used sometimes for loading images into the application whose pixels we need to read
 * (it is alternative to the rendering using WebGL and then readingPixels).
 */
var canvas2d;

/**
 * 2D context for canvas2d. This is actually variable which allows us to use methods of 2D.
 *
 * @type {CanvasRenderingContext2D}
 */
var ctx;

/**
 *
 * @type {Scene}
 */
var castleScene = new Scene();

/**
 * Controller (user input).
 *
 * @type {Control}
 */
var control;

/**
 * Loader of models, heightmaps and other stuff; also an create and chain function execution.
 *
 * @type {AssetLoader}
 */
var assetLoader;

/**
 * Application renderer.
 *
 * @type {Renderer}
 */
var renderer;

/**
 * User interface (creation, opening window dialogs, etc.).
 *
 * @type {Gui}
 */
var gui;

/**
 * Module for picking.
 *
 * @type {Picking}
 */
var picking;

/**
 * Collision detection system.
 *
 * @type {Collision}
 */
var collision;

/**
 * Reference to the avatar.
 *
 * @type {Entity}
 */
var player;

/**
 * Module for managing "high-level" heightmap collision detection (we have 3 layers, so we somehow need to decide
 * which layer to use etc.)
 *
 * @type {WorldHeightmap}
 */
var heightmapWorld;

/**
 * Measuring time and frames. Especially the measurements of frames are important, since some application logic
 * depends on that (e.g. collisions).
 *
 * @type {TimeMeasure}
 */
var clock;

/**
 * Class which governs the high-level tasks of the application - contains main loop, information about the
 * time when last frame was rendered, current entity which we control etc.
 *
 * @author smrceant (Antonin Smrcek)
 *
 * @constructor
 */
var Main = function()
{
  var _this = this;

  ////////////////////////////////////////////
  // APPLICATION WINDOW DIMENSIONS
  ////////////////////////////////////////////

  /**
   * This is the object which remembers how big is the window used for the rendering. Watch out, this may not be necessarily
   * the size of the window which you are looking at - it is simply the size in which the WebGL renders the scene.
   *
   * @type {Win}
   */
  this.windowRender = new Win (Preferences.Window.WINDOW_DEFAULT_USER_AVAILABLE_WIDTH, Preferences.Window.WINDOW_DEFAULT_USER_AVAILABLE_HEIGHT, function()
  {
    if (Util.isInitialized (gui))
    {
      Win.computeRatioToAnotherWindow (_this.windowRender, gui.getDisplayWindow(), gui.getWindowOriginalToRenderRatio());
    }
  });

  ////////////////////////////////////////////
  // LAST FRAME DELTA
  ////////////////////////////////////////////

  /**
   * Exact time when the last frame was rendered, obtained by Date.now().
   *
   * @type {number}
   */
  this.lastFrameTime = 0;

  /**
   * Difference in ms between current and last rendered frame.
   *
   * @type {number}
   */
  this.lastFrameDelta = 0;

  /**
   * By multiplicating the move amount with this value, we achieve that entities' move depend on time, not on number of rendered speed.
   *
   * @type {number}
   */
  this.lastFrameDeltaToSpeedBasisRatio = 0;

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

  /**
   * This is the entity through which we experience the virtual world. In this application, it is always the player.
   * It receives event of looking around (move angle).
   *
   * @private
   * @type {Entity}
   */
  this._currentEntity = null;

  /**
   * The entity which is controlled by keyboard/mouse/HUD input from the user. It does not have to be same as the
   * _currentEntity - image situation e.g. when you are standing on the roof, looking down on the courtyard,
   * but you are controlling little car which is riding around the courtyard, i.e. you can move it forward/backward
   * and strafe with it.
   *
   * So, to sum up: controlling entity will modify position of the bound entity when it is moved forward, backward
   * or strafed left/right. Looking around (move angle) is NEVER received by controllingEntity.
   *
   * In this application, it is always the player.
   *
   * @type {Entity}
   */
  this._currentControllingEntity = null;

  /**
   * TRUE if the main loop of the application is stopped. This means that nothing will happen, i.e. also rendering
   * is halted. Used when the user opens menu etc.
   *
   * @type {boolean}
   */
  this.mainLoopSuspended = false;

  /**
   * Current date obtained by Date.now().
   *
   * @type {number}
   * @private
   */
  this._tmp_currentDate = null;

  this._initialLightVisibilityFix = null;
};


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// LAST FRAME TIME DELTA
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Needed for initialization of the variable. Otherwise, first value of lastFrameDelta would be incorrect.
 */
Main.prototype.setInitialLastFrameTime = function ()
{
  this.lastFrameTime = Date.now();
};

/**
 * Retrieves number which shall be used in the computations regarding speed.
 *
 * @returns {number}
 */
Main.prototype.getLastFrameDeltaRatio = function ()
{
  return this.lastFrameDeltaToSpeedBasisRatio;
};

/**
 * Computes delta for the last frame.
 */
Main.prototype.computeLastFrameDelta = function ()
{
  this._tmp_currentDate = Date.now();

  this.lastFrameDelta = this._tmp_currentDate - this.lastFrameTime;

  this.lastFrameDeltaToSpeedBasisRatio = this.lastFrameDelta / Preferences.Application.FRAME_TIME_BASIS_FOR_SPEED_COMPUTATIONS;

  this.lastFrameTime = this._tmp_currentDate;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CURRENT ACTIVE ENTITIES GETTERS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Retrieves entity whose camera is using for viewing the virtual world.
 *
 * @returns {Entity}
 */
Main.prototype.getCurrentEntity = function ()
{
  return this._currentEntity;
};

/**
 * Retrieves entity which we are controlling (note that only actions which change POSITION (not angle) will be
 * taken into consideration).
 *
 * @returns {Entity}
 */
Main.prototype.getCurrentControllingEntity = function ()
{
  return this._currentControllingEntity;
};

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

/**
 * Retrieves window which has information about dimension of the CANVAS used for WebGL rendering.
 * This window is not the same as the browser window! It can be smaller (then the image is blurry, since browser has
 * to upscale it). It can be also bigger, then the image is antialised (it is a SSAA in fact, since we render image
 * e.g. in twice the resolution and browser has to make the image smaller).
 *
 * @returns {Win}
 */
Main.prototype.getRenderingWindow = function()
{
  return this.windowRender;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SUSPENSION OF THE MAIN LOOP
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Pauses the main loop, i.e. nothing will be computed, updated or drawn.
 */
Main.prototype.suspendMainLoop = function()
{
  this.mainLoopSuspended = true;
};

/**
 * Unpauses main loop, i.e. application will be "alive" again.
 */
Main.prototype.activateMainLoop = function()
{
  // otherwise, after we unpause the application, things like time would go fast-forward because the delta would be computed from all the inactive time
  this.lastFrameTime = Date.now();

  this.mainLoopSuspended = false;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SUSPENSION OF THE APPLICATION
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Pauses the execution of the application main loop, makes controls for the virtual walk unresponsive.
 */
Main.prototype.freezeApplication = function()
{
  this.suspendMainLoop();
  control.suspendKeyboard();
  control.suspendMouse();
};

/**
 * Unpauses the execution of the application main loop and makes controls for the virtual walk responsive again.
 */
Main.prototype.unfreezeApplication = function()
{
  this.activateMainLoop();
  control.activateKeyboard();
  control.activateMouse();
};

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

/**
 * Initializes Three.js widget for measuring FPS.
 */
Main.prototype.initStats = function()
{
  statsThree.setMode (0); // 0: fps, 1: ms, 2: mb
  document.body.appendChild (statsThree.domElement);
};

/**
 * Main loop of the application, performed each frame. Enables entities to move, checks collisions, applies gravity,
 * performs picking and draws objects.
 */
Main.prototype.mainLoop = function()
{
  requestAnimationFrame (appMain.mainLoop.bind (appMain));

  statsThree.begin();
  clock.startFrame();

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

  if (appMain.mainLoopSuspended)
  {
    return;
  }

  /////////////////////////////
  // TIME DELTA COMPUTATION
  /////////////////////////////

  appMain.computeLastFrameDelta();

  /////////////////////////////
  // TIME SIMULATION
  /////////////////////////////

  VisualEffects.Sky.updateTimeSimulation();

  ////////////////////////////////////////////////////////////////
  // INFO CUBES SPINNING
  ////////////////////////////////////////////////////////////////

  CustomThreeShaderMaterials.info_cube.uniforms.u_Time.value = clock.getElapsedTime();


  ////////////////////////////////////////////////////////////////
  // WATER ANIMATION
  ////////////////////////////////////////////////////////////////

  castleScene.animateWater (appMain.lastFrameDelta);

  ////////////////////////////////////////////////////////////////
  // MOVING + COLLISIONS/GRAVITY
  ////////////////////////////////////////////////////////////////

  // set correct collision limits depending on if we are in a tunnel or not
  if (player.inTunnel)
  {
    player.timeInTunnel += this.lastFrameDelta;

    Preferences.Collision.RIGHT_DIRECTION_DISTANCE_LIMIT = Preferences.Collision.RIGHT_DIRECTION_DISTANCE_LIMIT_TUNNEL;
  }
  else
  {
    player.timeInTunnel = 0;

    Preferences.Collision.RIGHT_DIRECTION_DISTANCE_LIMIT = Preferences.Collision.RIGHT_DIRECTION_DISTANCE_LIMIT_NORMAL;
  }

  collision._stagMoatFix();

  // there was collision in the last frame, so we need to restore CSS of virtual keyboard back to normal .. but ONLY if the collision in the least frame was not from mouse wheel event, which needs special treatment
  if (
      ( (clock.elapsedFrames - 1 === collision.lastCollisionFrameId) && (clock.elapsedFrames - 1    !== collision.keepForbiddenKeyboardForFrameId) ) ||
      (clock.elapsedFrames - Preferences.Control.MOUSE_WHEEL_COLLISION_FRAMES_FORBIDDEN_KEYBOARD_COLORING >= collision.keepForbiddenKeyboardForFrameId) // wait a few frames until we can clear the style, otherwise the red keyboard would not be visible long enough
      )
  {
    gui.restoreVirtualKeyboardNormalCss();
  }

  // apply gravity only on the player
  if (appMain.getCurrentEntity() === player)
  {
    collision.freefall (player);
  }

  // check if active entity should move (and perform collision detection)
  control.keyboardContinuous();

  ////////////////////////////////////////////////////////////////
  // CAMERA UPDATE
  ////////////////////////////////////////////////////////////////

	if(Tour.IsActive()){
		Tour.CameraPosition(appMain.lastFrameDelta * 0.001, appMain.getCurrentEntity().camera);
	}
	else{
		// must happen after all collisions, movements etc. (when it is determined whether it is legal to move entity or not)
		appMain.getCurrentEntity().camera.updateCamera();
	}

	// Send updated camera position to multiplayer environment
	Multiplayer.Update(appMain.getCurrentEntity().camera.position.clone());

  ////////////////////////////////////////////////////////////////
  // PICKING, DRAWING, MINIMAP UPDATE
  ////////////////////////////////////////////////////////////////
  picking.checkPickingState();

  renderer.render();

  gui.updateMinimap (player);

  clock.elapsedFrames++;

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

  clock.endFrame();
  statsThree.end();

};

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

/**
 * Main class of the application.
 *
 * @type {Main}
 */
var appMain = new Main();

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MAIN
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Application entry point, called when HTML page with application is loaded. Initializes WebGL and Canvas2D context of
 * the canvas(es), initializes global variables and downloads and processes scene graph.
 */
function main()
{
  try
  {
		clock = new TimeMeasure(true);

		gui = new Gui();

		assetLoader  = new AssetLoader();

		// we have to create CANVAS elements now
		gui.initCanvases();

		//////////////////////////////////////////////////////
		// CLASS INITIALIZATIONS
		//////////////////////////////////////////////////////

		// (beware of the order of creation)

		// create player with default camera parameters
		player = new Entity (new Camera (null, null), "player");

		appMain._currentEntity = player;
		appMain._currentControllingEntity = player;

		control      = new Control();
		renderer     = new Renderer();

		picking = new Picking();

		appMain.mainLoading();

        Multiplayer.Init();
  }
  catch (exception)
  {
    console.error (exception);
  }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// LOADING AND INITIALIZATION OF THE APPLICATION
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Loads scene, shaders etc., and inits everything needed for the application.
 */
Main.prototype.mainLoading = function()
{
  try
  {
    //////////////////////////////////////////////////////
    // ASSET LOADING
    //////////////////////////////////////////////////////

    // IMPORTANT: be really careful about the order... for example, it is important to call assetScene.waitFor (guiLoading)
    // as soon as the assetScene and guiLoading jobs are defined, because jobs like meshpack etc. wait for assetScene and
    // if we wouldn't waitFor guiLoading right away and we start jobs like meshpack, they would start the assetScene job right away - and there is the problem: the asset scene does not yet wait for the guiLoading

    // attach init function to root - this will be executed when the entire world is prepared
    var assetROOT = assetLoader.makeDummyJob("world creation", true, appMain.onWorldLoad.bind (appMain));

    var rendererInit = assetLoader.makeDummyJob("renderer init", true, function()
    {
      renderer.init();
    });

    //////////////////////////////////////////////////////
    // SCENE LOAD
    //////////////////////////////////////////////////////

    var modelsLoadingFinished = assetLoader.makeExternalLoadingJob("3D models for scene loading");
    var modelsLoading = assetLoader.makeDummyJob("models loading", false, function()
    {
      castleScene.loadScene(Preferences.Application.SCENE_DEFINITION_FILE, modelsLoadingFinished);
    });
    assetROOT.waitFor(modelsLoading);

    //////////////////////////////////////////////////////
    // SHADER FILES DOWNLOAD
    //////////////////////////////////////////////////////

    // note: we could have defined shaders in strings, but since it's a huge pain to write shaders like that and we have the asset loader functionality, we make a good use of it!

    /**
     * Helper function, so it is more simple to load shader programs. It creates an asset loader job and makes assetROOT
     * wait for it. Assumes vertex and fragments shaders to have same filenames (apart from suffix) and to reside in shaders/ directory.
     *
     * @param   {string}    justShaderName                      e.g. "depth_shader" (NO "depth_shader.vert" or "shaders/color.frag")
     * @param   {Object}    [shaderOptions ]                    options for shader material, currently contains only .uniforms propery, which is assigned directly into ShaderMaterial.uniforms
     * @param   {Function}  [additionalShaderMaterialCallback]  additional function called when shader material from VS and FS files is created, its input argument is creates shader material
     */
    var loadShader = function (justShaderName, shaderOptions, additionalShaderMaterialCallback)
    {
      // load VS and FS, and if no custom callback which specifies how the shader material shall be created is supplied, perform the default callback
      var shader = assetLoader.loadFilesForShaderProgram("shaders/" + justShaderName + ".vert", "shaders/" + justShaderName + ".frag", function()
      {
        CustomThreeShaderMaterials[justShaderName] = new THREE.ShaderMaterial
        ({
          uniforms:       Util.isInitialized (shaderOptions) ? (Util.isInitialized (shaderOptions.uniforms) ? shaderOptions.uniforms : {}) : {},
          vertexShader:   AssetLoader.loadedFiles["shaders/" + justShaderName + ".vert"].getText(),
          fragmentShader: AssetLoader.loadedFiles["shaders/" + justShaderName + ".frag"].getText()
        });

        if (Util.isInitialized(additionalShaderMaterialCallback))
        {
          additionalShaderMaterialCallback (CustomThreeShaderMaterials[justShaderName]);
        }
      });

      rendererInit.waitFor (shader);

      return shader;
    };

    // ALL CUSTOM SHADERS ARE LOADED HERE:
    var heightmapGenerationShader = loadShader("distance_to_rgba"); // not necessary to var, but we need this exact shader in heightmap generation and we must be sure that it is ready
    loadShader("picking");
    //loadShader("object_outline");
    var infoCubeShader = loadShader("info_cube",
        {
          uniforms:
          {
            u_Time: { type: "f", value: Date.now() }
          }
        },
        // as an additional callback, assign texture to the material
        function (shaderMaterial)
        {
          var loader = new THREE.TextureLoader();

          loader.load (Preferences.Picking.INFO_CUBE_TEXTURE, function (texture)
          {
            shaderMaterial.map = texture;
            shaderMaterial.needsUpdate = true;

            shaderMaterial.uniforms.u_Texture = { type: "t", value: texture };
          });
        }
    );

    //modelsLoading.waitFor (infoCubeShader);
    modelsLoading.waitFor (rendererInit);

    //////////////////////////////////////////////////////
    // GUI INIT
    //////////////////////////////////////////////////////

    var guiLoading = assetLoader.makeDummyJob("whole GUI loading", false);
    assetROOT.waitFor(guiLoading);

    var guiCreation = assetLoader.makeDummyJob("GUI class creation", false, function ()
    {
      gui.init(guiLoading);
    });
    //guiCreation.waitFor (worldMapLoaded);
    //assetROOT.waitFor (guiCreation);
    guiLoading.waitFor(guiCreation);

    //////////////////////////////////////////////////////
    // WORLD MAP LOAD
    //////////////////////////////////////////////////////

    var worldMapLoaded = assetLoader.makeDummyJob("worldmap - all heightmaps loading finished", false, function ()
    {
      if (Preferences.Application.isDevMode())
      {
        console.log("worldMapLoaded at " + Date.now());
      }

      // now with heightmap loaded, we can init scene and collision system
      // since we couldn't have optimized the scene because of the heightmap generation, optimize it now
      castleScene.optimizeScene (true);

      collision = new Collision (heightmapWorld);
    });
    assetROOT.waitFor(worldMapLoaded);

    guiCreation.waitFor(worldMapLoaded);

    var worldMapHeightmapsLoading = assetLoader.makeDummyJob("worldmap - all heightmaps ready to load", false, function ()
    {
      if (Preferences.Application.isDevMode())
      {
        console.log("worldMapHeightmapsLoading enter at: " + Date.now());
      }

      var worldMapCreate = WorldHeightmap.createMap();

      worldMapHeightmapsLoading.waitFor (worldMapCreate);
    });

    //  problem: it executes external job modelsLoadingFinished() -> we shall keep the .mustStartItself property and use it in case of external jobs to prevent them from running like this via waitFor
    //               or is it better to leave like this? i.e. is it understandable enough that we should use waitWithRunningForFinish() because of the waitFor() behavior? Probably not, those external jobs are really special cases.
    //worldMapHeightmapsLoading.waitFor(modelsLoadingFinished); // we need models ready for heightmap
    worldMapHeightmapsLoading.waitWithRunningForFinish(modelsLoadingFinished); // we need models ready for heightmap

    //worldMapHeightmapsLoading.waitFor(heightmapGenerationShader); // we need special shader for heightmap generation
    worldMapHeightmapsLoading.waitFor(rendererInit);

    worldMapLoaded.waitFor(worldMapHeightmapsLoading);

    //////////////////////////////////////////////////////
    // EXECUTING
    //////////////////////////////////////////////////////

    assetROOT.startJob();
  }
  catch (exception)
  {
    console.error(exception);
  }
};

/**
 * Executed after the whole scene is done loading.
 */
Main.prototype.onWorldLoad = function()
{
  if (Preferences.Application.MUTE_CONSOLE)
  {
    // clear anything which is in the console (even if we mute it, Three.js and jQuery somehow still manage to write into it.. maybe they captured it in the closure or something like that)
    if (console.clear)
    {
      // (wait a little, those Three.js messages popup after a little while)
      setTimeout(console.clear, 1500);
    }
  }

  if (Preferences.Browser.EDGE)
  {
	$.notify(ContentLibrary.getContentString("EDGE_BROWSER"), {
		 autoHideDelay: 20000
	});
  }

  // tries to fix messed up czech accent characters in SVG (weird, but it is OK when we switch languages back and forth)
  if (Preferences.Browser.EDGE)
  {
	var curLanguage = Preferences.Languages.getCurrentLanguage();

	if (curLanguage === Preferences.Languages.CZ)
	{
	  Preferences.Languages.changeLanguageToEN();
	  Preferences.Languages.changeLanguageToCZ();
	}
  }

  Preferences.Viewpoint.VIEWPOINT_DEFAULT.setAsCurrentViewpoint();

  if (Preferences.Application.isDevMode())
  {
    console.log("On world load: " + Date.now());
  }

  // activate when heightmaps and stuff are done
  VisualEffects.Sky.activate();

  appMain.setInitialLastFrameTime();

  setTimeout (function()
  {
    // trying to overcome the fact that minimap image (red dot position specifically) is a little messed up until it loads, computes for the first time and stuff..
    gui.minimap.show();
  }, 300);


  this.initStats();

  clock.startBenchmark();

  // start the application main loop!
  appMain.mainLoop();

  gui.showCanvas();
};
