/**
 * Provide WebGL layer to zrender. Which is rendered on top of clay.
 *
 *
 * Relationship between zrender, LayerGL(renderer) and ViewGL(Scene, Camera, Viewport)
 *           zrender
 *           /     \
 *      LayerGL   LayerGL
 *    (renderer) (renderer)
 *      /     \
 *  ViewGL   ViewGL
 */
import * as echarts from 'echarts/lib/echarts';
import Renderer from 'claygl/src/Renderer';
import RayPicking from 'claygl/src/picking/RayPicking';
import Texture from 'claygl/src/Texture';
import graphicGL from '../util/graphicGL'; // PENDING, clay. notifier is same with zrender Eventful

import notifier from 'claygl/src/core/mixin/notifier';
import requestAnimationFrame from 'zrender/lib/animation/requestAnimationFrame';
/**
 * @constructor
 * @alias module:echarts-gl/core/LayerGL
 * @param {string} id Layer ID
 * @param {module:zrender/ZRender} zr
 */

var LayerGL = function (id, zr) {
  /**
   * Layer ID
   * @type {string}
   */
  this.id = id;
  /**
   * @type {module:zrender/ZRender}
   */

  this.zr = zr;
  /**
   * @type {clay.Renderer}
   */

  try {
    this.renderer = new Renderer({
      clearBit: 0,
      devicePixelRatio: zr.painter.dpr,
      preserveDrawingBuffer: true,
      // PENDING
      premultipliedAlpha: true
    });
    this.renderer.resize(zr.painter.getWidth(), zr.painter.getHeight());
  } catch (e) {
    this.renderer = null;
    this.dom = document.createElement('div');
    this.dom.style.cssText = 'position:absolute; left: 0; top: 0; right: 0; bottom: 0;';
    this.dom.className = 'ecgl-nowebgl';
    this.dom.innerHTML = 'Sorry, your browser does not support WebGL';
    console.error(e);
    return;
  }

  this.onglobalout = this.onglobalout.bind(this);
  zr.on('globalout', this.onglobalout);
  /**
   * Canvas dom for webgl rendering
   * @type {HTMLCanvasElement}
   */

  this.dom = this.renderer.canvas;
  var style = this.dom.style;
  style.position = 'absolute';
  style.left = '0';
  style.top = '0';
  /**
   * @type {Array.<clay.Scene>}
   */

  this.views = [];
  this._picking = new RayPicking({
    renderer: this.renderer
  });
  this._viewsToDispose = [];
  /**
   * Current accumulating id.
   */

  this._accumulatingId = 0;
  this._zrEventProxy = new echarts.graphic.Rect({
    shape: {
      x: -1,
      y: -1,
      width: 2,
      height: 2
    },
    // FIXME Better solution.
    __isGLToZRProxy: true
  });
  this._backgroundColor = null;
  this._disposed = false;
};

LayerGL.prototype.setUnpainted = function () {};
/**
 * @param {module:echarts-gl/core/ViewGL} view
 */


LayerGL.prototype.addView = function (view) {
  if (view.layer === this) {
    return;
  } // If needs to dispose in this layer. unmark it.


  var idx = this._viewsToDispose.indexOf(view);

  if (idx >= 0) {
    this._viewsToDispose.splice(idx, 1);
  }

  this.views.push(view);
  view.layer = this;
  var zr = this.zr;
  view.scene.traverse(function (node) {
    node.__zr = zr;

    if (node.addAnimatorsToZr) {
      node.addAnimatorsToZr(zr);
    }
  });
};

function removeFromZr(node) {
  var zr = node.__zr;
  node.__zr = null;

  if (zr && node.removeAnimatorsFromZr) {
    node.removeAnimatorsFromZr(zr);
  }
}
/**
 * @param {module:echarts-gl/core/ViewGL} view
 */


LayerGL.prototype.removeView = function (view) {
  if (view.layer !== this) {
    return;
  }

  var idx = this.views.indexOf(view);

  if (idx >= 0) {
    this.views.splice(idx, 1);
    view.scene.traverse(removeFromZr, this);
    view.layer = null; // Mark to dispose in this layer.

    this._viewsToDispose.push(view);
  }
};
/**
 * Remove all views
 */


LayerGL.prototype.removeViewsAll = function () {
  this.views.forEach(function (view) {
    view.scene.traverse(removeFromZr, this);
    view.layer = null; // Mark to dispose in this layer.

    this._viewsToDispose.push(view);
  }, this);
  this.views.length = 0;
};
/**
 * Resize the canvas and viewport, will be invoked by zrender
 * @param  {number} width
 * @param  {number} height
 */


LayerGL.prototype.resize = function (width, height) {
  var renderer = this.renderer;
  renderer.resize(width, height);
};
/**
 * Clear color and depth
 * @return {[type]} [description]
 */


LayerGL.prototype.clear = function () {
  var gl = this.renderer.gl;
  var clearColor = this._backgroundColor || [0, 0, 0, 0];
  gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
  gl.depthMask(true);
  gl.colorMask(true, true, true, true);
  gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT);
};
/**
 * Clear depth
 */


LayerGL.prototype.clearDepth = function () {
  var gl = this.renderer.gl;
  gl.clear(gl.DEPTH_BUFFER_BIT);
};
/**
 * Clear color
 */


LayerGL.prototype.clearColor = function () {
  var gl = this.renderer.gl;
  gl.clearColor(0, 0, 0, 0);
  gl.clear(gl.COLOR_BUFFER_BIT);
};
/**
 * Mark layer to refresh next tick
 */


LayerGL.prototype.needsRefresh = function () {
  this.zr.refresh();
};
/**
 * Refresh the layer, will be invoked by zrender
 */


LayerGL.prototype.refresh = function (bgColor) {
  this._backgroundColor = bgColor ? graphicGL.parseColor(bgColor) : [0, 0, 0, 0];
  this.renderer.clearColor = this._backgroundColor;

  for (var i = 0; i < this.views.length; i++) {
    this.views[i].prepareRender(this.renderer);
  }

  this._doRender(false); // Auto dispose unused resources on GPU, like program(shader), texture, geometry(buffers)


  this._trackAndClean(); // Dispose trashed views


  for (var i = 0; i < this._viewsToDispose.length; i++) {
    this._viewsToDispose[i].dispose(this.renderer);
  }

  this._viewsToDispose.length = 0;

  this._startAccumulating();
};

LayerGL.prototype.renderToCanvas = function (ctx) {
  // PENDING will block the page
  this._startAccumulating(true);

  ctx.drawImage(this.dom, 0, 0, ctx.canvas.width, ctx.canvas.height);
};

LayerGL.prototype._doRender = function (accumulating) {
  this.clear();
  this.renderer.saveViewport();

  for (var i = 0; i < this.views.length; i++) {
    this.views[i].render(this.renderer, accumulating);
  }

  this.renderer.restoreViewport();
};
/**
 * Stop accumulating
 */


LayerGL.prototype._stopAccumulating = function () {
  this._accumulatingId = 0;
  clearTimeout(this._accumulatingTimeout);
};

var accumulatingId = 1;
/**
 * Start accumulating all the views.
 * Accumulating is for antialising and have more sampling in SSAO
 * @private
 */

LayerGL.prototype._startAccumulating = function (immediate) {
  var self = this;

  this._stopAccumulating();

  var needsAccumulate = false;

  for (var i = 0; i < this.views.length; i++) {
    needsAccumulate = this.views[i].needsAccumulate() || needsAccumulate;
  }

  if (!needsAccumulate) {
    return;
  }

  function accumulate(id) {
    if (!self._accumulatingId || id !== self._accumulatingId) {
      return;
    }

    var isFinished = true;

    for (var i = 0; i < self.views.length; i++) {
      isFinished = self.views[i].isAccumulateFinished() && needsAccumulate;
    }

    if (!isFinished) {
      self._doRender(true);

      if (immediate) {
        accumulate(id);
      } else {
        requestAnimationFrame(function () {
          accumulate(id);
        });
      }
    }
  }

  this._accumulatingId = accumulatingId++;

  if (immediate) {
    accumulate(self._accumulatingId);
  } else {
    this._accumulatingTimeout = setTimeout(function () {
      accumulate(self._accumulatingId);
    }, 50);
  }
};

LayerGL.prototype._trackAndClean = function () {
  var textureList = [];
  var geometriesList = []; // Mark all resources unused;

  if (this._textureList) {
    markUnused(this._textureList);
    markUnused(this._geometriesList);
  }

  for (var i = 0; i < this.views.length; i++) {
    collectResources(this.views[i].scene, textureList, geometriesList);
  } // Dispose those unsed resources.


  if (this._textureList) {
    checkAndDispose(this.renderer, this._textureList);
    checkAndDispose(this.renderer, this._geometriesList);
  }

  this._textureList = textureList;
  this._geometriesList = geometriesList;
};

function markUnused(resourceList) {
  for (var i = 0; i < resourceList.length; i++) {
    resourceList[i].__used__ = 0;
  }
}

function checkAndDispose(renderer, resourceList) {
  for (var i = 0; i < resourceList.length; i++) {
    if (!resourceList[i].__used__) {
      resourceList[i].dispose(renderer);
    }
  }
}

function updateUsed(resource, list) {
  resource.__used__ = resource.__used__ || 0;
  resource.__used__++;

  if (resource.__used__ === 1) {
    // Don't push to the list twice.
    list.push(resource);
  }
}

function collectResources(scene, textureResourceList, geometryResourceList) {
  var prevMaterial;
  var prevGeometry;
  scene.traverse(function (renderable) {
    if (renderable.isRenderable()) {
      var geometry = renderable.geometry;
      var material = renderable.material; // TODO optimize!!

      if (material !== prevMaterial) {
        var textureUniforms = material.getTextureUniforms();

        for (var u = 0; u < textureUniforms.length; u++) {
          var uniformName = textureUniforms[u];
          var val = material.uniforms[uniformName].value;

          if (!val) {
            continue;
          }

          if (val instanceof Texture) {
            updateUsed(val, textureResourceList);
          } else if (val instanceof Array) {
            for (var k = 0; k < val.length; k++) {
              if (val[k] instanceof Texture) {
                updateUsed(val[k], textureResourceList);
              }
            }
          }
        }
      }

      if (geometry !== prevGeometry) {
        updateUsed(geometry, geometryResourceList);
      }

      prevMaterial = material;
      prevGeometry = geometry;
    }
  });

  for (var k = 0; k < scene.lights.length; k++) {
    // Track AmbientCubemap
    if (scene.lights[k].cubemap) {
      updateUsed(scene.lights[k].cubemap, textureResourceList);
    }
  }
}
/**
 * Dispose the layer
 */


LayerGL.prototype.dispose = function () {
  if (this._disposed) {
    return;
  }

  this._stopAccumulating();

  if (this._textureList) {
    markUnused(this._textureList);
    markUnused(this._geometriesList);
    checkAndDispose(this.renderer, this._textureList);
    checkAndDispose(this.renderer, this._geometriesList);
  }

  this.zr.off('globalout', this.onglobalout);
  this._disposed = true;
}; // Event handlers


LayerGL.prototype.onmousedown = function (e) {
  if (e.target && e.target.__isGLToZRProxy) {
    return;
  }

  e = e.event;
  var obj = this.pickObject(e.offsetX, e.offsetY);

  if (obj) {
    this._dispatchEvent('mousedown', e, obj);

    this._dispatchDataEvent('mousedown', e, obj);
  }

  this._downX = e.offsetX;
  this._downY = e.offsetY;
};

LayerGL.prototype.onmousemove = function (e) {
  if (e.target && e.target.__isGLToZRProxy) {
    return;
  }

  e = e.event;
  var obj = this.pickObject(e.offsetX, e.offsetY);
  var target = obj && obj.target;
  var lastHovered = this._hovered;
  this._hovered = obj;

  if (lastHovered && target !== lastHovered.target) {
    lastHovered.relatedTarget = target;

    this._dispatchEvent('mouseout', e, lastHovered); // this._dispatchDataEvent('mouseout', e, lastHovered);


    this.zr.setCursorStyle('default');
  }

  this._dispatchEvent('mousemove', e, obj);

  if (obj) {
    this.zr.setCursorStyle('pointer');

    if (!lastHovered || target !== lastHovered.target) {
      this._dispatchEvent('mouseover', e, obj); // this._dispatchDataEvent('mouseover', e, obj);

    }
  }

  this._dispatchDataEvent('mousemove', e, obj);
};

LayerGL.prototype.onmouseup = function (e) {
  if (e.target && e.target.__isGLToZRProxy) {
    return;
  }

  e = e.event;
  var obj = this.pickObject(e.offsetX, e.offsetY);

  if (obj) {
    this._dispatchEvent('mouseup', e, obj);

    this._dispatchDataEvent('mouseup', e, obj);
  }

  this._upX = e.offsetX;
  this._upY = e.offsetY;
};

LayerGL.prototype.onclick = LayerGL.prototype.dblclick = function (e) {
  if (e.target && e.target.__isGLToZRProxy) {
    return;
  } // Ignore click event if mouse moved


  var dx = this._upX - this._downX;
  var dy = this._upY - this._downY;

  if (Math.sqrt(dx * dx + dy * dy) > 20) {
    return;
  }

  e = e.event;
  var obj = this.pickObject(e.offsetX, e.offsetY);

  if (obj) {
    this._dispatchEvent(e.type, e, obj);

    this._dispatchDataEvent(e.type, e, obj);
  } // Try set depth of field onclick


  var result = this._clickToSetFocusPoint(e);

  if (result) {
    var success = result.view.setDOFFocusOnPoint(result.distance);

    if (success) {
      this.zr.refresh();
    }
  }
};

LayerGL.prototype._clickToSetFocusPoint = function (e) {
  var renderer = this.renderer;
  var oldViewport = renderer.viewport;

  for (var i = this.views.length - 1; i >= 0; i--) {
    var viewGL = this.views[i];

    if (viewGL.hasDOF() && viewGL.containPoint(e.offsetX, e.offsetY)) {
      this._picking.scene = viewGL.scene;
      this._picking.camera = viewGL.camera; // Only used for picking, renderer.setViewport will also invoke gl.viewport.
      // Set directly, PENDING.

      renderer.viewport = viewGL.viewport;

      var result = this._picking.pick(e.offsetX, e.offsetY, true);

      if (result) {
        result.view = viewGL;
        return result;
      }
    }
  }

  renderer.viewport = oldViewport;
};

LayerGL.prototype.onglobalout = function (e) {
  var lastHovered = this._hovered;

  if (lastHovered) {
    this._dispatchEvent('mouseout', e, {
      target: lastHovered.target
    });
  }
};

LayerGL.prototype.pickObject = function (x, y) {
  var output = [];
  var renderer = this.renderer;
  var oldViewport = renderer.viewport;

  for (var i = 0; i < this.views.length; i++) {
    var viewGL = this.views[i];

    if (viewGL.containPoint(x, y)) {
      this._picking.scene = viewGL.scene;
      this._picking.camera = viewGL.camera; // Only used for picking, renderer.setViewport will also invoke gl.viewport.
      // Set directly, PENDING.

      renderer.viewport = viewGL.viewport;

      this._picking.pickAll(x, y, output);
    }
  }

  renderer.viewport = oldViewport;
  output.sort(function (a, b) {
    return a.distance - b.distance;
  });
  return output[0];
};

LayerGL.prototype._dispatchEvent = function (eveName, originalEvent, newEvent) {
  if (!newEvent) {
    newEvent = {};
  }

  var current = newEvent.target;
  newEvent.cancelBubble = false;
  newEvent.event = originalEvent;
  newEvent.type = eveName;
  newEvent.offsetX = originalEvent.offsetX;
  newEvent.offsetY = originalEvent.offsetY;

  while (current) {
    current.trigger(eveName, newEvent);
    current = current.getParent();

    if (newEvent.cancelBubble) {
      break;
    }
  }

  this._dispatchToView(eveName, newEvent);
};

LayerGL.prototype._dispatchDataEvent = function (eveName, originalEvent, newEvent) {
  var mesh = newEvent && newEvent.target;
  var dataIndex = mesh && mesh.dataIndex;
  var seriesIndex = mesh && mesh.seriesIndex; // Custom event data

  var eventData = mesh && mesh.eventData;
  var elChangedInMouseMove = false;
  var eventProxy = this._zrEventProxy;
  eventProxy.x = originalEvent.offsetX;
  eventProxy.y = originalEvent.offsetY;
  eventProxy.update();
  var targetInfo = {
    target: eventProxy
  };
  const ecData = echarts.helper.getECData(eventProxy);

  if (eveName === 'mousemove') {
    if (dataIndex != null) {
      if (dataIndex !== this._lastDataIndex) {
        if (parseInt(this._lastDataIndex, 10) >= 0) {
          ecData.dataIndex = this._lastDataIndex;
          ecData.seriesIndex = this._lastSeriesIndex; // FIXME May cause double events.

          this.zr.handler.dispatchToElement(targetInfo, 'mouseout', originalEvent);
        }

        elChangedInMouseMove = true;
      }
    } else if (eventData != null) {
      if (eventData !== this._lastEventData) {
        if (this._lastEventData != null) {
          ecData.eventData = this._lastEventData; // FIXME May cause double events.

          this.zr.handler.dispatchToElement(targetInfo, 'mouseout', originalEvent);
        }

        elChangedInMouseMove = true;
      }
    }

    this._lastEventData = eventData;
    this._lastDataIndex = dataIndex;
    this._lastSeriesIndex = seriesIndex;
  }

  ecData.eventData = eventData;
  ecData.dataIndex = dataIndex;
  ecData.seriesIndex = seriesIndex;

  if (eventData != null || parseInt(dataIndex, 10) >= 0 && parseInt(seriesIndex, 10) >= 0) {
    this.zr.handler.dispatchToElement(targetInfo, eveName, originalEvent);

    if (elChangedInMouseMove) {
      this.zr.handler.dispatchToElement(targetInfo, 'mouseover', originalEvent);
    }
  }
};

LayerGL.prototype._dispatchToView = function (eventName, e) {
  for (var i = 0; i < this.views.length; i++) {
    if (this.views[i].containPoint(e.offsetX, e.offsetY)) {
      this.views[i].trigger(eventName, e);
    }
  }
};

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