/*
 * @module echarts-gl/core/ViewGL
 * @author Yi Shen(http://github.com/pissang)
 */
import * as echarts from 'echarts/lib/echarts';
import Scene from 'claygl/src/Scene';
import ShadowMapPass from 'claygl/src/prePass/ShadowMap';
import PerspectiveCamera from 'claygl/src/camera/Perspective';
import OrthographicCamera from 'claygl/src/camera/Orthographic';
import Matrix4 from 'claygl/src/math/Matrix4';
import Vector3 from 'claygl/src/math/Vector3';
import Vector2 from 'claygl/src/math/Vector2';
import notifier from 'claygl/src/core/mixin/notifier';
import EffectCompositor from '../effect/EffectCompositor';
import TemporalSuperSampling from '../effect/TemporalSuperSampling';
import halton from '../effect/halton';
/**
 * @constructor
 * @alias module:echarts-gl/core/ViewGL
 * @param {string} [projection='perspective']
 */

function ViewGL(projection) {
  projection = projection || 'perspective';
  /**
   * @type {module:echarts-gl/core/LayerGL}
   */

  this.layer = null;
  /**
   * @type {clay.Scene}
   */

  this.scene = new Scene();
  /**
   * @type {clay.Node}
   */

  this.rootNode = this.scene;
  this.viewport = {
    x: 0,
    y: 0,
    width: 0,
    height: 0
  };
  this.setProjection(projection);
  this._compositor = new EffectCompositor();
  this._temporalSS = new TemporalSuperSampling();
  this._shadowMapPass = new ShadowMapPass();
  var pcfKernels = [];
  var off = 0;

  for (var i = 0; i < 30; i++) {
    var pcfKernel = [];

    for (var k = 0; k < 6; k++) {
      pcfKernel.push(halton(off, 2) * 4.0 - 2.0);
      pcfKernel.push(halton(off, 3) * 4.0 - 2.0);
      off++;
    }

    pcfKernels.push(pcfKernel);
  }

  this._pcfKernels = pcfKernels;
  this.scene.on('beforerender', function (renderer, scene, camera) {
    if (this.needsTemporalSS()) {
      this._temporalSS.jitterProjection(renderer, camera);
    }
  }, this);
}
/**
 * Set camera type of group
 * @param {string} cameraType 'perspective' | 'orthographic'
 */


ViewGL.prototype.setProjection = function (projection) {
  var oldCamera = this.camera;
  oldCamera && oldCamera.update();

  if (projection === 'perspective') {
    if (!(this.camera instanceof PerspectiveCamera)) {
      this.camera = new PerspectiveCamera();

      if (oldCamera) {
        this.camera.setLocalTransform(oldCamera.localTransform);
      }
    }
  } else {
    if (!(this.camera instanceof OrthographicCamera)) {
      this.camera = new OrthographicCamera();

      if (oldCamera) {
        this.camera.setLocalTransform(oldCamera.localTransform);
      }
    }
  } // PENDING


  this.camera.near = 0.1;
  this.camera.far = 2000;
};
/**
 * Set viewport of group
 * @param {number} x Viewport left bottom x
 * @param {number} y Viewport left bottom y
 * @param {number} width Viewport height
 * @param {number} height Viewport height
 * @param {number} [dpr=1]
 */


ViewGL.prototype.setViewport = function (x, y, width, height, dpr) {
  if (this.camera instanceof PerspectiveCamera) {
    this.camera.aspect = width / height;
  }

  dpr = dpr || 1;
  this.viewport.x = x;
  this.viewport.y = y;
  this.viewport.width = width;
  this.viewport.height = height;
  this.viewport.devicePixelRatio = dpr; // Source and output of compositor use high dpr texture.
  // But the intermediate texture of bloom, dof effects use fixed 1.0 dpr

  this._compositor.resize(width * dpr, height * dpr);

  this._temporalSS.resize(width * dpr, height * dpr);
};
/**
 * If contain screen point x, y
 * @param {number} x offsetX
 * @param {number} y offsetY
 * @return {boolean}
 */


ViewGL.prototype.containPoint = function (x, y) {
  var viewport = this.viewport;
  var height = this.layer.renderer.getHeight(); // Flip y;

  y = height - y;
  return x >= viewport.x && y >= viewport.y && x <= viewport.x + viewport.width && y <= viewport.y + viewport.height;
};
/**
 * Cast a ray
 * @param {number} x offsetX
 * @param {number} y offsetY
 * @param {clay.math.Ray} out
 * @return {clay.math.Ray}
 */


var ndc = new Vector2();

ViewGL.prototype.castRay = function (x, y, out) {
  var renderer = this.layer.renderer;
  var oldViewport = renderer.viewport;
  renderer.viewport = this.viewport;
  renderer.screenToNDC(x, y, ndc);
  this.camera.castRay(ndc, out);
  renderer.viewport = oldViewport;
  return out;
};
/**
 * Prepare and update scene before render
 */


ViewGL.prototype.prepareRender = function () {
  this.scene.update();
  this.camera.update();
  this.scene.updateLights();
  var renderList = this.scene.updateRenderList(this.camera);
  this._needsSortProgressively = false; // If has any transparent mesh needs sort triangles progressively.

  for (var i = 0; i < renderList.transparent.length; i++) {
    var renderable = renderList.transparent[i];
    var geometry = renderable.geometry;

    if (geometry.needsSortVerticesProgressively && geometry.needsSortVerticesProgressively()) {
      this._needsSortProgressively = true;
    }

    if (geometry.needsSortTrianglesProgressively && geometry.needsSortTrianglesProgressively()) {
      this._needsSortProgressively = true;
    }
  }

  this._frame = 0;

  this._temporalSS.resetFrame(); // var lights = this.scene.getLights();
  // for (var i = 0; i < lights.length; i++) {
  //     if (lights[i].cubemap) {
  //         if (this._compositor && this._compositor.isSSREnabled()) {
  //             lights[i].invisible = true;
  //         }
  //         else {
  //             lights[i].invisible = false;
  //         }
  //     }
  // }

};

ViewGL.prototype.render = function (renderer, accumulating) {
  this._doRender(renderer, accumulating, this._frame);

  this._frame++;
};

ViewGL.prototype.needsAccumulate = function () {
  return this.needsTemporalSS() || this._needsSortProgressively;
};

ViewGL.prototype.needsTemporalSS = function () {
  var enableTemporalSS = this._enableTemporalSS;

  if (enableTemporalSS === 'auto') {
    enableTemporalSS = this._enablePostEffect;
  }

  return enableTemporalSS;
};

ViewGL.prototype.hasDOF = function () {
  return this._enableDOF;
};

ViewGL.prototype.isAccumulateFinished = function () {
  return this.needsTemporalSS() ? this._temporalSS.isFinished() : this._frame > 30;
};

ViewGL.prototype._doRender = function (renderer, accumulating, accumFrame) {
  var scene = this.scene;
  var camera = this.camera;
  accumFrame = accumFrame || 0;

  this._updateTransparent(renderer, scene, camera, accumFrame);

  if (!accumulating) {
    this._shadowMapPass.kernelPCF = this._pcfKernels[0]; // Not render shadowmap pass in accumulating frame.

    this._shadowMapPass.render(renderer, scene, camera, true);
  }

  this._updateShadowPCFKernel(accumFrame); // Shadowmap will set clear color.


  var bgColor = renderer.clearColor;
  renderer.gl.clearColor(bgColor[0], bgColor[1], bgColor[2], bgColor[3]);

  if (this._enablePostEffect) {
    // normal render also needs to be jittered when have edge pass.
    if (this.needsTemporalSS()) {
      this._temporalSS.jitterProjection(renderer, camera);
    }

    this._compositor.updateNormal(renderer, scene, camera, this._temporalSS.getFrame());
  } // Always update SSAO to make sure have correct ssaoMap status


  this._updateSSAO(renderer, scene, camera, this._temporalSS.getFrame());

  if (this._enablePostEffect) {
    var frameBuffer = this._compositor.getSourceFrameBuffer();

    frameBuffer.bind(renderer);
    renderer.gl.clear(renderer.gl.DEPTH_BUFFER_BIT | renderer.gl.COLOR_BUFFER_BIT);
    renderer.render(scene, camera, true, true);
    frameBuffer.unbind(renderer);

    if (this.needsTemporalSS() && accumulating) {
      this._compositor.composite(renderer, scene, camera, this._temporalSS.getSourceFrameBuffer(), this._temporalSS.getFrame());

      renderer.setViewport(this.viewport);

      this._temporalSS.render(renderer);
    } else {
      renderer.setViewport(this.viewport);

      this._compositor.composite(renderer, scene, camera, null, 0);
    }
  } else {
    if (this.needsTemporalSS() && accumulating) {
      var frameBuffer = this._temporalSS.getSourceFrameBuffer();

      frameBuffer.bind(renderer);
      renderer.saveClear();
      renderer.clearBit = renderer.gl.DEPTH_BUFFER_BIT | renderer.gl.COLOR_BUFFER_BIT;
      renderer.render(scene, camera, true, true);
      renderer.restoreClear();
      frameBuffer.unbind(renderer);
      renderer.setViewport(this.viewport);

      this._temporalSS.render(renderer);
    } else {
      renderer.setViewport(this.viewport);
      renderer.render(scene, camera, true, true);
    }
  } // this._shadowMapPass.renderDebug(renderer);
  // this._compositor._normalPass.renderDebug(renderer);

};

ViewGL.prototype._updateTransparent = function (renderer, scene, camera, frame) {
  var v3 = new Vector3();
  var invWorldTransform = new Matrix4();
  var cameraWorldPosition = camera.getWorldPosition();
  var transparentList = scene.getRenderList(camera).transparent; // Sort transparent object.

  for (var i = 0; i < transparentList.length; i++) {
    var renderable = transparentList[i];
    var geometry = renderable.geometry;
    Matrix4.invert(invWorldTransform, renderable.worldTransform);
    Vector3.transformMat4(v3, cameraWorldPosition, invWorldTransform);

    if (geometry.needsSortTriangles && geometry.needsSortTriangles()) {
      geometry.doSortTriangles(v3, frame);
    }

    if (geometry.needsSortVertices && geometry.needsSortVertices()) {
      geometry.doSortVertices(v3, frame);
    }
  }
};

ViewGL.prototype._updateSSAO = function (renderer, scene, camera) {
  var ifEnableSSAO = this._enableSSAO && this._enablePostEffect;

  if (ifEnableSSAO) {
    this._compositor.updateSSAO(renderer, scene, camera, this._temporalSS.getFrame());
  }

  var renderList = scene.getRenderList(camera);

  for (var i = 0; i < renderList.opaque.length; i++) {
    var renderable = renderList.opaque[i]; // PENDING

    if (renderable.renderNormal) {
      renderable.material[ifEnableSSAO ? 'enableTexture' : 'disableTexture']('ssaoMap');
    }

    if (ifEnableSSAO) {
      renderable.material.set('ssaoMap', this._compositor.getSSAOTexture());
    }
  }
};

ViewGL.prototype._updateShadowPCFKernel = function (frame) {
  var pcfKernel = this._pcfKernels[frame % this._pcfKernels.length];
  var renderList = this.scene.getRenderList(this.camera);
  var opaqueList = renderList.opaque;

  for (var i = 0; i < opaqueList.length; i++) {
    if (opaqueList[i].receiveShadow) {
      opaqueList[i].material.set('pcfKernel', pcfKernel);
      opaqueList[i].material.define('fragment', 'PCF_KERNEL_SIZE', pcfKernel.length / 2);
    }
  }
};

ViewGL.prototype.dispose = function (renderer) {
  this._compositor.dispose(renderer.gl);

  this._temporalSS.dispose(renderer.gl);

  this._shadowMapPass.dispose(renderer);
};
/**
 * @param {module:echarts/Model} Post effect model
 */


ViewGL.prototype.setPostEffect = function (postEffectModel, api) {
  var compositor = this._compositor;
  this._enablePostEffect = postEffectModel.get('enable');
  var bloomModel = postEffectModel.getModel('bloom');
  var edgeModel = postEffectModel.getModel('edge');
  var dofModel = postEffectModel.getModel('DOF', postEffectModel.getModel('depthOfField'));
  var ssaoModel = postEffectModel.getModel('SSAO', postEffectModel.getModel('screenSpaceAmbientOcclusion'));
  var ssrModel = postEffectModel.getModel('SSR', postEffectModel.getModel('screenSpaceReflection'));
  var fxaaModel = postEffectModel.getModel('FXAA');
  var colorCorrModel = postEffectModel.getModel('colorCorrection');
  bloomModel.get('enable') ? compositor.enableBloom() : compositor.disableBloom();
  dofModel.get('enable') ? compositor.enableDOF() : compositor.disableDOF();
  ssrModel.get('enable') ? compositor.enableSSR() : compositor.disableSSR();
  colorCorrModel.get('enable') ? compositor.enableColorCorrection() : compositor.disableColorCorrection();
  edgeModel.get('enable') ? compositor.enableEdge() : compositor.disableEdge();
  fxaaModel.get('enable') ? compositor.enableFXAA() : compositor.disableFXAA();
  this._enableDOF = dofModel.get('enable');
  this._enableSSAO = ssaoModel.get('enable');
  this._enableSSAO ? compositor.enableSSAO() : compositor.disableSSAO();
  compositor.setBloomIntensity(bloomModel.get('intensity'));
  compositor.setEdgeColor(edgeModel.get('color'));
  compositor.setColorLookupTexture(colorCorrModel.get('lookupTexture'), api);
  compositor.setExposure(colorCorrModel.get('exposure'));
  ['radius', 'quality', 'intensity'].forEach(function (name) {
    compositor.setSSAOParameter(name, ssaoModel.get(name));
  });
  ['quality', 'maxRoughness', 'physical'].forEach(function (name) {
    compositor.setSSRParameter(name, ssrModel.get(name));
  });
  ['quality', 'focalDistance', 'focalRange', 'blurRadius', 'fstop'].forEach(function (name) {
    compositor.setDOFParameter(name, dofModel.get(name));
  });
  ['brightness', 'contrast', 'saturation'].forEach(function (name) {
    compositor.setColorCorrection(name, colorCorrModel.get(name));
  });
};

ViewGL.prototype.setDOFFocusOnPoint = function (depth) {
  if (this._enablePostEffect) {
    if (depth > this.camera.far || depth < this.camera.near) {
      return;
    }

    this._compositor.setDOFParameter('focalDistance', depth);

    return true;
  }
};

ViewGL.prototype.setTemporalSuperSampling = function (temporalSuperSamplingModel) {
  this._enableTemporalSS = temporalSuperSamplingModel.get('enable');
};

ViewGL.prototype.isLinearSpace = function () {
  return this._enablePostEffect;
};

ViewGL.prototype.setRootNode = function (rootNode) {
  if (this.rootNode === rootNode) {
    return;
  }

  var children = this.rootNode.children();

  for (var i = 0; i < children.length; i++) {
    rootNode.add(children[i]);
  }

  if (rootNode !== this.scene) {
    this.scene.add(rootNode);
  }

  this.rootNode = rootNode;
}; // Proxies


ViewGL.prototype.add = function (node3D) {
  this.rootNode.add(node3D);
};

ViewGL.prototype.remove = function (node3D) {
  this.rootNode.remove(node3D);
};

ViewGL.prototype.removeAll = function (node3D) {
  this.rootNode.removeAll(node3D);
};

Object.assign(ViewGL.prototype, notifier);
export default ViewGL;