// @ts-check
/*
 * GDevelop JS Platform
 * Copyright 2013-2016 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
 * This project is released under the MIT License.
 */

/**
 * Represents a layer of a scene, used to display objects.
 *
 * Viewports and multiple cameras are not supported.
 *
 * @class Layer
 * @param {LayerData} layerData The data used to initialize the layer
 * @param {gdjs.RuntimeScene} runtimeScene The scene in which the layer is used
 * @memberof gdjs
 */
gdjs.Layer = function(layerData, runtimeScene) {
  this._name = layerData.name;
  this._cameraRotation = 0;
  this._zoomFactor = 1;
  this._timeScale = 1;
  this._hidden = !layerData.visibility;
  this._initialEffectsData = layerData.effects || [];
  this._cameraX = runtimeScene.getGame().getGameResolutionWidth() / 2;
  this._cameraY = runtimeScene.getGame().getGameResolutionHeight() / 2;
  this._cachedGameResolutionWidth = runtimeScene
    .getGame()
    .getGameResolutionWidth();
  this._cachedGameResolutionHeight = runtimeScene
    .getGame()
    .getGameResolutionHeight();
  this._runtimeScene = runtimeScene;

  // @ts-ignore - assume the proper renderer is passed
  this._renderer = new gdjs.LayerRenderer(this, runtimeScene.getRenderer());
  this.show(!this._hidden);

  for(var i = 0;i < layerData.effects.length;++i) {
    this.addEffect(layerData.effects[i]);
  }
};

gdjs.Layer.prototype.getRenderer = function() {
  return this._renderer;
};

/**
 * Called by the RuntimeScene whenever the game resolution size is changed.
 * Updates the layer width/height and position.
 */
gdjs.Layer.prototype.onGameResolutionResized = function() {
  var oldGameResolutionWidth = this._cachedGameResolutionWidth;
  var oldGameResolutionHeight = this._cachedGameResolutionHeight;
  this._cachedGameResolutionWidth = this._runtimeScene
    .getGame()
    .getGameResolutionWidth();
  this._cachedGameResolutionHeight = this._runtimeScene
    .getGame()
    .getGameResolutionHeight();

  // Adapt position of the camera center as:
  // * Most cameras following a player/object on the scene will be updating this
  // in events anyway.
  // * Cameras not following a player/object are usually UIs which are intuitively
  // expected not to "move". Not adapting the center position would make the camera
  // move from its initial position (which is centered in the screen) - and anchor
  // behavior would behave counterintuitively.
  this._cameraX +=
    (this._cachedGameResolutionWidth - oldGameResolutionWidth) / 2;
  this._cameraY +=
    (this._cachedGameResolutionHeight - oldGameResolutionHeight) / 2;
  this._renderer.updatePosition();
};

/**
 * Returns the scene the layer belongs to
 * @returns {gdjs.RuntimeScene} the scene the layer belongs to
 */
gdjs.Layer.prototype.getRuntimeScene = function() {
  return this._runtimeScene;
};

/**
 * Called at each frame, after events are run and before rendering.
 * @param {gdjs.RuntimeScene} runtimeScene The scene the layer belongs to.
 */
gdjs.Layer.prototype.update = function(runtimeScene) {
  return this._renderer.updateTime();
};

/**
 * Get the name of the layer
 * @return {String} The name of the layer
 */
gdjs.Layer.prototype.getName = function() {
  return this._name;
};

/**
 * Change the camera center X position.
 *
 * @param {number=} cameraId The camera number. Currently ignored.
 * @return The x position of the camera
 */
gdjs.Layer.prototype.getCameraX = function(cameraId) {
  return this._cameraX;
};

/**
 * Change the camera center Y position.
 *
 * @param {number=} cameraId The camera number. Currently ignored.
 * @return The y position of the camera
 */
gdjs.Layer.prototype.getCameraY = function(cameraId) {
  return this._cameraY;
};

/**
 * Set the camera center X position.
 *
 * @param {number} x The new x position
 * @param {number=} cameraId The camera number. Currently ignored.
 */
gdjs.Layer.prototype.setCameraX = function(x, cameraId) {
  this._cameraX = x;
  this._renderer.updatePosition();
};

/**
 * Set the camera center Y position.
 *
 * @param {number} y The new y position
 * @param {number=} cameraId The camera number. Currently ignored.
 */
gdjs.Layer.prototype.setCameraY = function(y, cameraId) {
  this._cameraY = y;
  this._renderer.updatePosition();
};

/**
 * Get the camera width (which can be different than the game resolution width
 * if the camera is zoomed).
 *
 * @param {number=} cameraId The camera number. Currently ignored.
 * @return {number} The width of the camera
 */
gdjs.Layer.prototype.getCameraWidth = function(cameraId) {
  return (+this._cachedGameResolutionWidth * 1) / this._zoomFactor;
};

/**
 * Get the camera height (which can be different than the game resolution height
 * if the camera is zoomed).
 *
 * @param {number=} cameraId The camera number. Currently ignored.
 * @return {number} The height of the camera
 */
gdjs.Layer.prototype.getCameraHeight = function(cameraId) {
  return (+this._cachedGameResolutionHeight * 1) / this._zoomFactor;
};

/**
 * Show (or hide) the layer.
 * @param {boolean} enable true to show the layer, false to hide it.
 */
gdjs.Layer.prototype.show = function(enable) {
  this._hidden = !enable;
  this._renderer.updateVisibility(enable);
};

/**
 * Check if the layer is visible.
 *
 * @return true if the layer is visible.
 */
gdjs.Layer.prototype.isVisible = function() {
  return !this._hidden;
};

/**
 * Set the zoom of a camera.
 *
 * @param {number} newZoom The new zoom. Must be superior to 0. 1 is the default zoom.
 * @param {number=} cameraId The camera number. Currently ignored.
 */
gdjs.Layer.prototype.setCameraZoom = function(newZoom, cameraId) {
  this._zoomFactor = newZoom;
  this._renderer.updatePosition();
};

/**
 * Get the zoom of a camera.
 *
 * @param {number=} cameraId The camera number. Currently ignored.
 * @return {number} The zoom.
 */
gdjs.Layer.prototype.getCameraZoom = function(cameraId) {
  return this._zoomFactor;
};

/**
 * Get the rotation of the camera, expressed in degrees.
 *
 * @param {number=} cameraId The camera number. Currently ignored.
 * @return {number} The rotation, in degrees.
 */
gdjs.Layer.prototype.getCameraRotation = function(cameraId) {
  return this._cameraRotation;
};

/**
 * Set the rotation of the camera, expressed in degrees.
 * The rotation is made around the camera center.
 *
 * @param {number} rotation The new rotation, in degrees.
 * @param {number=} cameraId The camera number. Currently ignored.
 */
gdjs.Layer.prototype.setCameraRotation = function(rotation, cameraId) {
  this._cameraRotation = rotation;
  this._renderer.updatePosition();
};

/**
 * Convert a point from the canvas coordinates (For example, the mouse position) to the
 * "world" coordinates.
 *
 * TODO: Update this method to store the result in a static array
 *
 * @param {number} x The x position, in canvas coordinates.
 * @param {number} y The y position, in canvas coordinates.
 * @param {number=} cameraId The camera number. Currently ignored.
 */
gdjs.Layer.prototype.convertCoords = function(x, y, cameraId) {
  x -= this._cachedGameResolutionWidth / 2;
  y -= this._cachedGameResolutionHeight / 2;
  x /= Math.abs(this._zoomFactor);
  y /= Math.abs(this._zoomFactor);

  // Only compute angle and cos/sin once (allow heavy optimization from JS engines).
  var angleInRadians = (this._cameraRotation / 180) * Math.PI;
  var tmp = x;
  var cosValue = Math.cos(angleInRadians);
  var sinValue = Math.sin(angleInRadians);
  x = cosValue * x - sinValue * y;
  y = sinValue * tmp + cosValue * y;

  return [x + this.getCameraX(cameraId), y + this.getCameraY(cameraId)];
};

gdjs.Layer.prototype.convertInverseCoords = function(x, y, cameraId) {
  x -= this.getCameraX(cameraId);
  y -= this.getCameraY(cameraId);

  // Only compute angle and cos/sin once (allow heavy optimization from JS engines).
  var angleInRadians = (this._cameraRotation / 180) * Math.PI;
  var tmp = x;
  var cosValue = Math.cos(-angleInRadians);
  var sinValue = Math.sin(-angleInRadians);
  x = cosValue * x - sinValue * y;
  y = sinValue * tmp + cosValue * y;

  x *= Math.abs(this._zoomFactor);
  y *= Math.abs(this._zoomFactor);

  return [
    x + this._cachedGameResolutionWidth / 2,
    y + this._cachedGameResolutionHeight / 2,
  ];
};

gdjs.Layer.prototype.getWidth = function() {
  return this._cachedGameResolutionWidth;
};

gdjs.Layer.prototype.getHeight = function() {
  return this._cachedGameResolutionHeight;
};

/**
 * Return the initial effects data for the layer. Only to
 * be used by renderers.
 */
gdjs.Layer.prototype.getInitialEffectsData = function() {
  return this._initialEffectsData;
};

/**
 * Add a new effect, or replace the one with the same name.
 * @param {EffectData} effectData The data of the effect to add.
 */
gdjs.Layer.prototype.addEffect = function(effectData) {
  this._renderer.addEffect(effectData);

  for (var name in effectData.doubleParameters) {
    this.setEffectDoubleParameter(
      effectData.name,
      name,
      effectData.doubleParameters[name]
    );
  }
  for (var name in effectData.stringParameters) {
    this.setEffectStringParameter(
      effectData.name,
      name,
      effectData.stringParameters[name]
    );
  }
  for (var name in effectData.booleanParameters) {
    this.setEffectBooleanParameter(
      effectData.name,
      name,
      effectData.booleanParameters[name]
    );
  }
}

/**
 * Remove the effect with the specified name
 * @param {string} effectName The name of the effect.
 */
gdjs.Layer.prototype.removeEffect = function(effectName) {
  this._renderer.removeEffect(effectName);
}

/**
 * Change an effect parameter value (for parameters that are numbers).
 * @param {string} name The name of the effect to update.
 * @param {string} parameterName The name of the parameter to update.
 * @param {number} value The new value (number).
 */
gdjs.Layer.prototype.setEffectDoubleParameter = function(
  name,
  parameterName,
  value
) {
  return this._renderer.setEffectDoubleParameter(name, parameterName, value);
};

/**
 * Change an effect parameter value (for parameters that are strings).
 * @param {string} name The name of the effect to update.
 * @param {string} parameterName The name of the parameter to update.
 * @param {string} value The new value (string).
 */
gdjs.Layer.prototype.setEffectStringParameter = function(
  name,
  parameterName,
  value
) {
  return this._renderer.setEffectStringParameter(name, parameterName, value);
};

/**
 * Change an effect parameter value (for parameters that are booleans).
 * @param {string} name The name of the effect to update.
 * @param {string} parameterName The name of the parameter to update.
 * @param {boolean} value The new value (boolean).
 */
gdjs.Layer.prototype.setEffectBooleanParameter = function(
  name,
  parameterName,
  value
) {
  return this._renderer.setEffectBooleanParameter(name, parameterName, value);
};

/**
 * Enable or disable an effect.
 * @param {string} name The name of the effect to enable or disable.
 * @param {boolean} enable true to enable, false to disable
 */
gdjs.Layer.prototype.enableEffect = function(name, enable) {
  this._renderer.enableEffect(name, enable);
};

/**
 * Check if an effect is enabled
 * @param {string} name The name of the effect
 * @return {boolean} true if the effect is enabled, false otherwise.
 */
gdjs.Layer.prototype.isEffectEnabled = function(name) {
  return this._renderer.isEffectEnabled(name);
};

/**
 * Check if an effect exists on this layer
 * @param {string} name The name of the effect
 * @return {boolean} true if the effect exists, false otherwise.
 */
gdjs.Layer.prototype.hasEffect = function(name) {
  return this._renderer.hasEffect(name);
};

/**
 * Set the time scale for the objects on the layer:
 * time will be slower if time scale is < 1, faster if > 1.
 * @param {number} timeScale The new time scale (must be positive).
 */
gdjs.Layer.prototype.setTimeScale = function(timeScale) {
  if (timeScale >= 0) this._timeScale = timeScale;
};

/**
 * Get the time scale for the objects on the layer.
 */
gdjs.Layer.prototype.getTimeScale = function() {
  return this._timeScale;
};

/**
 * Return the time elapsed since the last frame,
 * in milliseconds, for objects on the layer.
 */
gdjs.Layer.prototype.getElapsedTime = function() {
  return this._runtimeScene.getTimeManager().getElapsedTime() * this._timeScale;
};
