import Shader from 'claygl/src/Shader';
import Texture2D from 'claygl/src/Texture2D';
import Texture from 'claygl/src/Texture';
import FrameBuffer from 'claygl/src/FrameBuffer';
import createCompositor from 'claygl/src/compositor/createCompositor';
import SSAOPass from './SSAOPass';
import SSRPass from './SSRPass';
import poissonKernel from './poissonKernel';
import graphicGL from '../util/graphicGL';
import NormalPass from './NormalPass';
import EdgePass from './EdgePass';
import effectJson from './composite.js';
import blurCode from 'claygl/src/shader/source/compositor/blur.glsl.js';
import lutCode from 'claygl/src/shader/source/compositor/lut.glsl.js';
import outputCode from 'claygl/src/shader/source/compositor/output.glsl.js';
import brightCode from 'claygl/src/shader/source/compositor/bright.glsl.js';
import downsampleCode from 'claygl/src/shader/source/compositor/downsample.glsl.js';
import upsampleCode from 'claygl/src/shader/source/compositor/upsample.glsl.js';
import hdrCode from 'claygl/src/shader/source/compositor/hdr.glsl.js';
import blendCode from 'claygl/src/shader/source/compositor/blend.glsl.js';
import fxaaCode from 'claygl/src/shader/source/compositor/fxaa.glsl.js';
import DOFCode from './DOF.glsl.js';
import edgeCode from './edge.glsl.js';
Shader['import'](blurCode);
Shader['import'](lutCode);
Shader['import'](outputCode);
Shader['import'](brightCode);
Shader['import'](downsampleCode);
Shader['import'](upsampleCode);
Shader['import'](hdrCode);
Shader['import'](blendCode);
Shader['import'](fxaaCode);
Shader['import'](DOFCode);
Shader['import'](edgeCode);

function makeCommonOutputs(getWidth, getHeight) {
  return {
    color: {
      parameters: {
        width: getWidth,
        height: getHeight
      }
    }
  };
}

var FINAL_NODES_CHAIN = ['composite', 'FXAA'];

function EffectCompositor() {
  this._width;
  this._height;
  this._dpr;
  this._sourceTexture = new Texture2D({
    type: Texture.HALF_FLOAT
  });
  this._depthTexture = new Texture2D({
    format: Texture.DEPTH_COMPONENT,
    type: Texture.UNSIGNED_INT
  });
  this._framebuffer = new FrameBuffer();

  this._framebuffer.attach(this._sourceTexture);

  this._framebuffer.attach(this._depthTexture, FrameBuffer.DEPTH_ATTACHMENT);

  this._normalPass = new NormalPass();
  this._compositor = createCompositor(effectJson);

  var sourceNode = this._compositor.getNodeByName('source');

  sourceNode.texture = this._sourceTexture;

  var cocNode = this._compositor.getNodeByName('coc');

  this._sourceNode = sourceNode;
  this._cocNode = cocNode;
  this._compositeNode = this._compositor.getNodeByName('composite');
  this._fxaaNode = this._compositor.getNodeByName('FXAA');
  this._dofBlurNodes = ['dof_far_blur', 'dof_near_blur', 'dof_coc_blur'].map(function (name) {
    return this._compositor.getNodeByName(name);
  }, this);
  this._dofBlurKernel = 0;
  this._dofBlurKernelSize = new Float32Array(0);
  this._finalNodesChain = FINAL_NODES_CHAIN.map(function (name) {
    return this._compositor.getNodeByName(name);
  }, this);
  var gBufferObj = {
    normalTexture: this._normalPass.getNormalTexture(),
    depthTexture: this._normalPass.getDepthTexture()
  };
  this._ssaoPass = new SSAOPass(gBufferObj);
  this._ssrPass = new SSRPass(gBufferObj);
  this._edgePass = new EdgePass(gBufferObj);
}

EffectCompositor.prototype.resize = function (width, height, dpr) {
  dpr = dpr || 1;
  var width = width * dpr;
  var height = height * dpr;
  var sourceTexture = this._sourceTexture;
  var depthTexture = this._depthTexture;
  sourceTexture.width = width;
  sourceTexture.height = height;
  depthTexture.width = width;
  depthTexture.height = height;
  var rendererMock = {
    getWidth: function () {
      return width;
    },
    getHeight: function () {
      return height;
    },
    getDevicePixelRatio: function () {
      return dpr;
    }
  };

  function wrapCallback(obj, key) {
    if (typeof obj[key] === 'function') {
      var oldFunc = obj[key].__original || obj[key]; // Use viewport width/height instead of renderer width/height

      obj[key] = function (renderer) {
        return oldFunc.call(this, rendererMock);
      };

      obj[key].__original = oldFunc;
    }
  }

  this._compositor.nodes.forEach(function (node) {
    for (var outKey in node.outputs) {
      var parameters = node.outputs[outKey].parameters;

      if (parameters) {
        wrapCallback(parameters, 'width');
        wrapCallback(parameters, 'height');
      }
    }

    for (var paramKey in node.parameters) {
      wrapCallback(node.parameters, paramKey);
    }
  });

  this._width = width;
  this._height = height;
  this._dpr = dpr;
};

EffectCompositor.prototype.getWidth = function () {
  return this._width;
};

EffectCompositor.prototype.getHeight = function () {
  return this._height;
};

EffectCompositor.prototype._ifRenderNormalPass = function () {
  return this._enableSSAO || this._enableEdge || this._enableSSR;
};

EffectCompositor.prototype._getPrevNode = function (node) {
  var idx = FINAL_NODES_CHAIN.indexOf(node.name) - 1;
  var prevNode = this._finalNodesChain[idx];

  while (prevNode && !this._compositor.getNodeByName(prevNode.name)) {
    idx -= 1;
    prevNode = this._finalNodesChain[idx];
  }

  return prevNode;
};

EffectCompositor.prototype._getNextNode = function (node) {
  var idx = FINAL_NODES_CHAIN.indexOf(node.name) + 1;
  var nextNode = this._finalNodesChain[idx];

  while (nextNode && !this._compositor.getNodeByName(nextNode.name)) {
    idx += 1;
    nextNode = this._finalNodesChain[idx];
  }

  return nextNode;
};

EffectCompositor.prototype._addChainNode = function (node) {
  var prevNode = this._getPrevNode(node);

  var nextNode = this._getNextNode(node);

  if (!prevNode) {
    return;
  }

  node.inputs.texture = prevNode.name;

  if (nextNode) {
    node.outputs = makeCommonOutputs(this.getWidth.bind(this), this.getHeight.bind(this));
    nextNode.inputs.texture = node.name;
  } else {
    node.outputs = null;
  }

  this._compositor.addNode(node);
};

EffectCompositor.prototype._removeChainNode = function (node) {
  var prevNode = this._getPrevNode(node);

  var nextNode = this._getNextNode(node);

  if (!prevNode) {
    return;
  }

  if (nextNode) {
    prevNode.outputs = makeCommonOutputs(this.getWidth.bind(this), this.getHeight.bind(this));
    nextNode.inputs.texture = prevNode.name;
  } else {
    prevNode.outputs = null;
  }

  this._compositor.removeNode(node);
};
/**
 * Update normal
 */


EffectCompositor.prototype.updateNormal = function (renderer, scene, camera, frame) {
  if (this._ifRenderNormalPass()) {
    this._normalPass.update(renderer, scene, camera);
  }
};
/**
 * Render SSAO after render the scene, before compositing
 */


EffectCompositor.prototype.updateSSAO = function (renderer, scene, camera, frame) {
  this._ssaoPass.update(renderer, camera, frame);
};
/**
 * Enable SSAO effect
 */


EffectCompositor.prototype.enableSSAO = function () {
  this._enableSSAO = true;
};
/**
 * Disable SSAO effect
 */


EffectCompositor.prototype.disableSSAO = function () {
  this._enableSSAO = false;
};
/**
 * Enable SSR effect
 */


EffectCompositor.prototype.enableSSR = function () {
  this._enableSSR = true; // this._normalPass.enableTargetTexture3 = true;
};
/**
 * Disable SSR effect
 */


EffectCompositor.prototype.disableSSR = function () {
  this._enableSSR = false; // this._normalPass.enableTargetTexture3 = false;
};
/**
 * Render SSAO after render the scene, before compositing
 */


EffectCompositor.prototype.getSSAOTexture = function () {
  return this._ssaoPass.getTargetTexture();
};
/**
 * @return {clay.FrameBuffer}
 */


EffectCompositor.prototype.getSourceFrameBuffer = function () {
  return this._framebuffer;
};
/**
 * @return {clay.Texture2D}
 */


EffectCompositor.prototype.getSourceTexture = function () {
  return this._sourceTexture;
};
/**
 * Disable fxaa effect
 */


EffectCompositor.prototype.disableFXAA = function () {
  this._removeChainNode(this._fxaaNode);
};
/**
 * Enable fxaa effect
 */


EffectCompositor.prototype.enableFXAA = function () {
  this._addChainNode(this._fxaaNode);
};
/**
 * Enable bloom effect
 */


EffectCompositor.prototype.enableBloom = function () {
  this._compositeNode.inputs.bloom = 'bloom_composite';

  this._compositor.dirty();
};
/**
 * Disable bloom effect
 */


EffectCompositor.prototype.disableBloom = function () {
  this._compositeNode.inputs.bloom = null;

  this._compositor.dirty();
};
/**
 * Enable depth of field effect
 */


EffectCompositor.prototype.enableDOF = function () {
  this._compositeNode.inputs.texture = 'dof_composite';

  this._compositor.dirty();
};
/**
 * Disable depth of field effect
 */


EffectCompositor.prototype.disableDOF = function () {
  this._compositeNode.inputs.texture = 'source';

  this._compositor.dirty();
};
/**
 * Enable color correction
 */


EffectCompositor.prototype.enableColorCorrection = function () {
  this._compositeNode.define('COLOR_CORRECTION');

  this._enableColorCorrection = true;
};
/**
 * Disable color correction
 */


EffectCompositor.prototype.disableColorCorrection = function () {
  this._compositeNode.undefine('COLOR_CORRECTION');

  this._enableColorCorrection = false;
};
/**
 * Enable edge detection
 */


EffectCompositor.prototype.enableEdge = function () {
  this._enableEdge = true;
};
/**
 * Disable edge detection
 */


EffectCompositor.prototype.disableEdge = function () {
  this._enableEdge = false;
};
/**
 * Set bloom intensity
 * @param {number} value
 */


EffectCompositor.prototype.setBloomIntensity = function (value) {
  this._compositeNode.setParameter('bloomIntensity', value);
};

EffectCompositor.prototype.setSSAOParameter = function (name, value) {
  switch (name) {
    case 'quality':
      // PENDING
      var kernelSize = {
        low: 6,
        medium: 12,
        high: 32,
        ultra: 62
      }[value] || 12;

      this._ssaoPass.setParameter('kernelSize', kernelSize);

      break;

    case 'radius':
      this._ssaoPass.setParameter(name, value);

      this._ssaoPass.setParameter('bias', value / 200);

      break;

    case 'intensity':
      this._ssaoPass.setParameter(name, value);

      break;

    default:
      if (process.env.NODE_ENV !== 'production') {
        console.warn('Unkown SSAO parameter ' + name);
      }

  }
};

EffectCompositor.prototype.setDOFParameter = function (name, value) {
  switch (name) {
    case 'focalDistance':
    case 'focalRange':
    case 'fstop':
      this._cocNode.setParameter(name, value);

      break;

    case 'blurRadius':
      for (var i = 0; i < this._dofBlurNodes.length; i++) {
        this._dofBlurNodes[i].setParameter('blurRadius', value);
      }

      break;

    case 'quality':
      var kernelSize = {
        low: 4,
        medium: 8,
        high: 16,
        ultra: 32
      }[value] || 8;
      this._dofBlurKernelSize = kernelSize;

      for (var i = 0; i < this._dofBlurNodes.length; i++) {
        this._dofBlurNodes[i].pass.material.define('POISSON_KERNEL_SIZE', kernelSize);
      }

      this._dofBlurKernel = new Float32Array(kernelSize * 2);
      break;

    default:
      if (process.env.NODE_ENV !== 'production') {
        console.warn('Unkown DOF parameter ' + name);
      }

  }
};

EffectCompositor.prototype.setSSRParameter = function (name, value) {
  if (value == null) {
    return;
  }

  switch (name) {
    case 'quality':
      // PENDING
      var maxIteration = {
        low: 10,
        medium: 15,
        high: 30,
        ultra: 80
      }[value] || 20;
      var pixelStride = {
        low: 32,
        medium: 16,
        high: 8,
        ultra: 4
      }[value] || 16;

      this._ssrPass.setParameter('maxIteration', maxIteration);

      this._ssrPass.setParameter('pixelStride', pixelStride);

      break;

    case 'maxRoughness':
      this._ssrPass.setParameter('minGlossiness', Math.max(Math.min(1.0 - value, 1.0), 0.0));

      break;

    case 'physical':
      this.setPhysicallyCorrectSSR(value);
      break;

    default:
      console.warn('Unkown SSR parameter ' + name);
  }
};

EffectCompositor.prototype.setPhysicallyCorrectSSR = function (physical) {
  this._ssrPass.setPhysicallyCorrect(physical);
};
/**
 * Set color of edge
 */


EffectCompositor.prototype.setEdgeColor = function (value) {
  var color = graphicGL.parseColor(value);

  this._edgePass.setParameter('edgeColor', color);
};

EffectCompositor.prototype.setExposure = function (value) {
  this._compositeNode.setParameter('exposure', Math.pow(2, value));
};

EffectCompositor.prototype.setColorLookupTexture = function (image, api) {
  this._compositeNode.pass.material.setTextureImage('lut', this._enableColorCorrection ? image : 'none', api, {
    minFilter: graphicGL.Texture.NEAREST,
    magFilter: graphicGL.Texture.NEAREST,
    flipY: false
  });
};

EffectCompositor.prototype.setColorCorrection = function (type, value) {
  this._compositeNode.setParameter(type, value);
};

EffectCompositor.prototype.isSSREnabled = function () {
  return this._enableSSR;
};

EffectCompositor.prototype.composite = function (renderer, scene, camera, framebuffer, frame) {
  var sourceTexture = this._sourceTexture;
  var targetTexture = sourceTexture;

  if (this._enableEdge) {
    this._edgePass.update(renderer, camera, sourceTexture, frame);

    sourceTexture = targetTexture = this._edgePass.getTargetTexture();
  }

  if (this._enableSSR) {
    this._ssrPass.update(renderer, camera, sourceTexture, frame);

    targetTexture = this._ssrPass.getTargetTexture();

    this._ssrPass.setSSAOTexture(this._enableSSAO ? this._ssaoPass.getTargetTexture() : null); // var lights = scene.getLights();
    // for (var i = 0; i < lights.length; i++) {
    //     if (lights[i].cubemap) {
    //         this._ssrPass.setAmbientCubemap(lights[i].cubemap, lights[i].intensity);
    //     }
    // }

  }

  this._sourceNode.texture = targetTexture;

  this._cocNode.setParameter('depth', this._depthTexture);

  var blurKernel = this._dofBlurKernel;
  var blurKernelSize = this._dofBlurKernelSize;
  var frameAll = Math.floor(poissonKernel.length / 2 / blurKernelSize);
  var kernelOffset = frame % frameAll;

  for (var i = 0; i < blurKernelSize * 2; i++) {
    blurKernel[i] = poissonKernel[i + kernelOffset * blurKernelSize * 2];
  }

  for (var i = 0; i < this._dofBlurNodes.length; i++) {
    this._dofBlurNodes[i].setParameter('percent', frame / 30.0);

    this._dofBlurNodes[i].setParameter('poissonKernel', blurKernel);
  }

  this._cocNode.setParameter('zNear', camera.near);

  this._cocNode.setParameter('zFar', camera.far);

  this._compositor.render(renderer, framebuffer);
};

EffectCompositor.prototype.dispose = function (renderer) {
  this._sourceTexture.dispose(renderer);

  this._depthTexture.dispose(renderer);

  this._framebuffer.dispose(renderer);

  this._compositor.dispose(renderer);

  this._normalPass.dispose(renderer);

  this._ssaoPass.dispose(renderer);
};

export default EffectCompositor;