// TODO orthographic camera
import * as echarts from 'echarts/lib/echarts';
import { createTextStyle } from 'echarts/lib/label/labelStyle';
import graphicGL from '../../util/graphicGL';
import OrbitControl from '../../util/OrbitControl';
import Lines3DGeometry from '../../util/geometry/Lines3D';
import retrieve from '../../util/retrieve';
var firstNotNull = retrieve.firstNotNull;
import ZRTextureAtlasSurface from '../../util/ZRTextureAtlasSurface';
import SceneHelper from '../common/SceneHelper';
import Grid3DFace from './Grid3DFace';
import Grid3DAxis from './Grid3DAxis';
import LabelsMesh from '../../util/mesh/LabelsMesh';
import lines3DGLSL from '../../util/shader/lines3D.glsl.js';
graphicGL.Shader.import(lines3DGLSL);
var dimIndicesMap = {
  // Left to right
  x: 0,
  // Far to near
  y: 2,
  // Bottom to up
  z: 1
};
export default echarts.ComponentView.extend({
  type: 'grid3D',
  __ecgl__: true,
  init: function (ecModel, api) {
    var FACES = [// planeDim0, planeDim1, offsetDim, dir on dim3 axis(gl), plane.
    ['y', 'z', 'x', -1, 'left'], ['y', 'z', 'x', 1, 'right'], ['x', 'y', 'z', -1, 'bottom'], ['x', 'y', 'z', 1, 'top'], ['x', 'z', 'y', -1, 'far'], ['x', 'z', 'y', 1, 'near']];
    var DIMS = ['x', 'y', 'z'];
    var quadsMaterial = new graphicGL.Material({
      // transparent: true,
      shader: graphicGL.createShader('ecgl.color'),
      depthMask: false,
      transparent: true
    });
    var linesMaterial = new graphicGL.Material({
      // transparent: true,
      shader: graphicGL.createShader('ecgl.meshLines3D'),
      depthMask: false,
      transparent: true
    });
    quadsMaterial.define('fragment', 'DOUBLE_SIDED');
    quadsMaterial.define('both', 'VERTEX_COLOR');
    this.groupGL = new graphicGL.Node();
    this._control = new OrbitControl({
      zr: api.getZr()
    });

    this._control.init(); // Save mesh and other infos for each face.


    this._faces = FACES.map(function (faceInfo) {
      var face = new Grid3DFace(faceInfo, linesMaterial, quadsMaterial);
      this.groupGL.add(face.rootNode);
      return face;
    }, this); // Save mesh and other infos for each axis.

    this._axes = DIMS.map(function (dim) {
      var axis = new Grid3DAxis(dim, linesMaterial);
      this.groupGL.add(axis.rootNode);
      return axis;
    }, this);
    var dpr = api.getDevicePixelRatio(); // Texture surface for label.

    this._axisLabelSurface = new ZRTextureAtlasSurface({
      width: 256,
      height: 256,
      devicePixelRatio: dpr
    });

    this._axisLabelSurface.onupdate = function () {
      api.getZr().refresh();
    };

    this._axisPointerLineMesh = new graphicGL.Mesh({
      geometry: new Lines3DGeometry({
        useNativeLine: false
      }),
      material: linesMaterial,
      castShadow: false,
      // PENDING
      ignorePicking: true,
      renderOrder: 3
    });
    this.groupGL.add(this._axisPointerLineMesh);
    this._axisPointerLabelsSurface = new ZRTextureAtlasSurface({
      width: 128,
      height: 128,
      devicePixelRatio: dpr
    });
    this._axisPointerLabelsMesh = new LabelsMesh({
      ignorePicking: true,
      renderOrder: 4,
      castShadow: false
    });

    this._axisPointerLabelsMesh.material.set('textureAtlas', this._axisPointerLabelsSurface.getTexture());

    this.groupGL.add(this._axisPointerLabelsMesh);
    this._lightRoot = new graphicGL.Node();
    this._sceneHelper = new SceneHelper();

    this._sceneHelper.initLight(this._lightRoot);
  },
  render: function (grid3DModel, ecModel, api) {
    this._model = grid3DModel;
    this._api = api;
    var cartesian = grid3DModel.coordinateSystem; // Always have light.

    cartesian.viewGL.add(this._lightRoot);

    if (grid3DModel.get('show')) {
      cartesian.viewGL.add(this.groupGL);
    } else {
      cartesian.viewGL.remove(this.groupGL);
    } // cartesian.viewGL.setCameraType(grid3DModel.get('viewControl.projection'));


    var control = this._control;
    control.setViewGL(cartesian.viewGL);
    var viewControlModel = grid3DModel.getModel('viewControl');
    control.setFromViewControlModel(viewControlModel, 0);

    this._axisLabelSurface.clear();

    control.off('update');

    if (grid3DModel.get('show')) {
      this._faces.forEach(function (face) {
        face.update(grid3DModel, ecModel, api);
      }, this);

      this._axes.forEach(function (axis) {
        axis.update(grid3DModel, this._axisLabelSurface, api);
      }, this);
    }

    control.on('update', this._onCameraChange.bind(this, grid3DModel, api), this);

    this._sceneHelper.setScene(cartesian.viewGL.scene);

    this._sceneHelper.updateLight(grid3DModel); // Set post effect


    cartesian.viewGL.setPostEffect(grid3DModel.getModel('postEffect'), api);
    cartesian.viewGL.setTemporalSuperSampling(grid3DModel.getModel('temporalSuperSampling'));

    this._initMouseHandler(grid3DModel);
  },
  afterRender: function (grid3DModel, ecModel, api, layerGL) {
    // Create ambient cubemap after render because we need to know the renderer.
    // TODO
    var renderer = layerGL.renderer;

    this._sceneHelper.updateAmbientCubemap(renderer, grid3DModel, api);

    this._sceneHelper.updateSkybox(renderer, grid3DModel, api);
  },

  /**
   * showAxisPointer will be triggered by action.
   */
  showAxisPointer: function (grid3dModel, ecModel, api, payload) {
    this._doShowAxisPointer();

    this._updateAxisPointer(payload.value);
  },

  /**
   * hideAxisPointer will be triggered by action.
   */
  hideAxisPointer: function (grid3dModel, ecModel, api, payload) {
    this._doHideAxisPointer();
  },
  _initMouseHandler: function (grid3DModel) {
    var cartesian = grid3DModel.coordinateSystem;
    var viewGL = cartesian.viewGL; // TODO xAxis3D.axisPointer.show ?

    if (grid3DModel.get('show') && grid3DModel.get('axisPointer.show')) {
      viewGL.on('mousemove', this._updateAxisPointerOnMousePosition, this);
    } else {
      viewGL.off('mousemove', this._updateAxisPointerOnMousePosition);
    }
  },

  /**
   * Try find and show axisPointer on the intersect point
   * of mouse ray with grid plane.
   */
  _updateAxisPointerOnMousePosition: function (e) {
    // Ignore if mouse is on the element.
    if (e.target) {
      return;
    }

    var grid3DModel = this._model;
    var cartesian = grid3DModel.coordinateSystem;
    var viewGL = cartesian.viewGL;
    var ray = viewGL.castRay(e.offsetX, e.offsetY, new graphicGL.Ray());
    var nearestIntersectPoint;

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

      if (face.rootNode.invisible) {
        continue;
      } // Plane is not face the camera. flip it


      if (face.plane.normal.dot(viewGL.camera.worldTransform.z) < 0) {
        face.plane.normal.negate();
      }

      var point = ray.intersectPlane(face.plane);

      if (!point) {
        continue;
      }

      var axis0 = cartesian.getAxis(face.faceInfo[0]);
      var axis1 = cartesian.getAxis(face.faceInfo[1]);
      var idx0 = dimIndicesMap[face.faceInfo[0]];
      var idx1 = dimIndicesMap[face.faceInfo[1]];

      if (axis0.contain(point.array[idx0]) && axis1.contain(point.array[idx1])) {
        nearestIntersectPoint = point;
      }
    }

    if (nearestIntersectPoint) {
      var data = cartesian.pointToData(nearestIntersectPoint.array, [], true);

      this._updateAxisPointer(data);

      this._doShowAxisPointer();
    } else {
      this._doHideAxisPointer();
    }
  },
  _onCameraChange: function (grid3DModel, api) {
    if (grid3DModel.get('show')) {
      this._updateFaceVisibility();

      this._updateAxisLinePosition();
    }

    var control = this._control;
    api.dispatchAction({
      type: 'grid3DChangeCamera',
      alpha: control.getAlpha(),
      beta: control.getBeta(),
      distance: control.getDistance(),
      center: control.getCenter(),
      from: this.uid,
      grid3DId: grid3DModel.id
    });
  },

  /**
   * Update visibility of each face when camera view changed, front face will be invisible.
   * @private
   */
  _updateFaceVisibility: function () {
    var camera = this._control.getCamera();

    var viewSpacePos = new graphicGL.Vector3();
    camera.update();

    for (var idx = 0; idx < this._faces.length / 2; idx++) {
      var depths = [];

      for (var k = 0; k < 2; k++) {
        var face = this._faces[idx * 2 + k];
        face.rootNode.getWorldPosition(viewSpacePos);
        viewSpacePos.transformMat4(camera.viewMatrix);
        depths[k] = viewSpacePos.z;
      } // Set the front face invisible


      var frontIndex = depths[0] > depths[1] ? 0 : 1;
      var frontFace = this._faces[idx * 2 + frontIndex];
      var backFace = this._faces[idx * 2 + 1 - frontIndex]; // Update rotation.

      frontFace.rootNode.invisible = true;
      backFace.rootNode.invisible = false;
    }
  },

  /**
   * Update axis line position when camera view changed.
   * @private
   */
  _updateAxisLinePosition: function () {
    // Put xAxis, yAxis on x, y visible plane.
    // Put zAxis on the left.
    // TODO
    var cartesian = this._model.coordinateSystem;
    var xAxis = cartesian.getAxis('x');
    var yAxis = cartesian.getAxis('y');
    var zAxis = cartesian.getAxis('z');
    var top = zAxis.getExtentMax();
    var bottom = zAxis.getExtentMin();
    var left = xAxis.getExtentMin();
    var right = xAxis.getExtentMax();
    var near = yAxis.getExtentMax();
    var far = yAxis.getExtentMin();
    var xAxisNode = this._axes[0].rootNode;
    var yAxisNode = this._axes[1].rootNode;
    var zAxisNode = this._axes[2].rootNode;
    var faces = this._faces; // Notice: in cartesian up axis is z, but in webgl up axis is y.

    var xAxisZOffset = faces[4].rootNode.invisible ? far : near;
    var xAxisYOffset = faces[2].rootNode.invisible ? top : bottom;
    var yAxisXOffset = faces[0].rootNode.invisible ? left : right;
    var yAxisYOffset = faces[2].rootNode.invisible ? top : bottom;
    var zAxisXOffset = faces[0].rootNode.invisible ? right : left;
    var zAxisZOffset = faces[4].rootNode.invisible ? far : near;
    xAxisNode.rotation.identity();
    yAxisNode.rotation.identity();
    zAxisNode.rotation.identity();

    if (faces[4].rootNode.invisible) {
      this._axes[0].flipped = true;
      xAxisNode.rotation.rotateX(Math.PI);
    }

    if (faces[0].rootNode.invisible) {
      this._axes[1].flipped = true;
      yAxisNode.rotation.rotateZ(Math.PI);
    }

    if (faces[4].rootNode.invisible) {
      this._axes[2].flipped = true;
      zAxisNode.rotation.rotateY(Math.PI);
    }

    xAxisNode.position.set(0, xAxisYOffset, xAxisZOffset);
    yAxisNode.position.set(yAxisXOffset, yAxisYOffset, 0); // Actually z

    zAxisNode.position.set(zAxisXOffset, 0, zAxisZOffset); // Actually y

    xAxisNode.update();
    yAxisNode.update();
    zAxisNode.update();

    this._updateAxisLabelAlign();
  },

  /**
   * Update label align on axis when axisLine position changed.
   * @private
   */
  _updateAxisLabelAlign: function () {
    // var cartesian = this._model.coordinateSystem;
    var camera = this._control.getCamera();

    var coords = [new graphicGL.Vector4(), new graphicGL.Vector4()];
    var center = new graphicGL.Vector4();
    this.groupGL.getWorldPosition(center);
    center.w = 1.0;
    center.transformMat4(camera.viewMatrix).transformMat4(camera.projectionMatrix);
    center.x /= center.w;
    center.y /= center.w;

    this._axes.forEach(function (axisInfo) {
      var lineCoords = axisInfo.axisLineCoords;
      var labelGeo = axisInfo.labelsMesh.geometry;

      for (var i = 0; i < coords.length; i++) {
        coords[i].setArray(lineCoords[i]);
        coords[i].w = 1.0;
        coords[i].transformMat4(axisInfo.rootNode.worldTransform).transformMat4(camera.viewMatrix).transformMat4(camera.projectionMatrix);
        coords[i].x /= coords[i].w;
        coords[i].y /= coords[i].w;
      }

      var dx = coords[1].x - coords[0].x;
      var dy = coords[1].y - coords[0].y;
      var cx = (coords[1].x + coords[0].x) / 2;
      var cy = (coords[1].y + coords[0].y) / 2;
      var textAlign;
      var verticalAlign;

      if (Math.abs(dy / dx) < 0.5) {
        textAlign = 'center';
        verticalAlign = cy > center.y ? 'bottom' : 'top';
      } else {
        verticalAlign = 'middle';
        textAlign = cx > center.x ? 'left' : 'right';
      } // axis labels


      axisInfo.setSpriteAlign(textAlign, verticalAlign, this._api);
    }, this);
  },
  _doShowAxisPointer: function () {
    if (!this._axisPointerLineMesh.invisible) {
      return;
    }

    this._axisPointerLineMesh.invisible = false;
    this._axisPointerLabelsMesh.invisible = false;

    this._api.getZr().refresh();
  },
  _doHideAxisPointer: function () {
    if (this._axisPointerLineMesh.invisible) {
      return;
    }

    this._axisPointerLineMesh.invisible = true;
    this._axisPointerLabelsMesh.invisible = true;

    this._api.getZr().refresh();
  },

  /**
   * @private updateAxisPointer.
   */
  _updateAxisPointer: function (data) {
    var cartesian = this._model.coordinateSystem;
    var point = cartesian.dataToPoint(data);
    var axisPointerLineMesh = this._axisPointerLineMesh;
    var linesGeo = axisPointerLineMesh.geometry;

    var axisPointerParentModel = this._model.getModel('axisPointer');

    var dpr = this._api.getDevicePixelRatio();

    linesGeo.convertToDynamicArray(true);

    function ifShowAxisPointer(axis) {
      return retrieve.firstNotNull(axis.model.get('axisPointer.show'), axisPointerParentModel.get('show'));
    }

    function getAxisColorAndLineWidth(axis) {
      var axisPointerModel = axis.model.getModel('axisPointer', axisPointerParentModel);
      var lineStyleModel = axisPointerModel.getModel('lineStyle');
      var color = graphicGL.parseColor(lineStyleModel.get('color'));
      var lineWidth = firstNotNull(lineStyleModel.get('width'), 1);
      var opacity = firstNotNull(lineStyleModel.get('opacity'), 1);
      color[3] *= opacity;
      return {
        color: color,
        lineWidth: lineWidth
      };
    }

    for (var k = 0; k < this._faces.length; k++) {
      var face = this._faces[k];

      if (face.rootNode.invisible) {
        continue;
      }

      var faceInfo = face.faceInfo;
      var otherCoord = faceInfo[3] < 0 ? cartesian.getAxis(faceInfo[2]).getExtentMin() : cartesian.getAxis(faceInfo[2]).getExtentMax();
      var otherDimIdx = dimIndicesMap[faceInfo[2]]; // Line on face.

      for (var i = 0; i < 2; i++) {
        var dim = faceInfo[i];
        var faceOtherDim = faceInfo[1 - i];
        var axis = cartesian.getAxis(dim);
        var faceOtherAxis = cartesian.getAxis(faceOtherDim);

        if (!ifShowAxisPointer(axis)) {
          continue;
        }

        var p0 = [0, 0, 0];
        var p1 = [0, 0, 0];
        var dimIdx = dimIndicesMap[dim];
        var faceOtherDimIdx = dimIndicesMap[faceOtherDim];
        p0[dimIdx] = p1[dimIdx] = point[dimIdx];
        p0[otherDimIdx] = p1[otherDimIdx] = otherCoord;
        p0[faceOtherDimIdx] = faceOtherAxis.getExtentMin();
        p1[faceOtherDimIdx] = faceOtherAxis.getExtentMax();
        var colorAndLineWidth = getAxisColorAndLineWidth(axis);
        linesGeo.addLine(p0, p1, colorAndLineWidth.color, colorAndLineWidth.lineWidth * dpr);
      } // Project line.


      if (ifShowAxisPointer(cartesian.getAxis(faceInfo[2]))) {
        var p0 = point.slice();
        var p1 = point.slice();
        p1[otherDimIdx] = otherCoord;
        var colorAndLineWidth = getAxisColorAndLineWidth(cartesian.getAxis(faceInfo[2]));
        linesGeo.addLine(p0, p1, colorAndLineWidth.color, colorAndLineWidth.lineWidth * dpr);
      }
    }

    linesGeo.convertToTypedArray();

    this._updateAxisPointerLabelsMesh(data);

    this._api.getZr().refresh();
  },
  _updateAxisPointerLabelsMesh: function (data) {
    var grid3dModel = this._model;
    var axisPointerLabelsMesh = this._axisPointerLabelsMesh;
    var axisPointerLabelsSurface = this._axisPointerLabelsSurface;
    var cartesian = grid3dModel.coordinateSystem;
    var axisPointerParentModel = grid3dModel.getModel('axisPointer');
    axisPointerLabelsMesh.geometry.convertToDynamicArray(true);
    axisPointerLabelsSurface.clear();
    var otherDim = {
      x: 'y',
      y: 'x',
      z: 'y'
    };

    this._axes.forEach(function (axisInfo, idx) {
      var axis = cartesian.getAxis(axisInfo.dim);
      var axisModel = axis.model;
      var axisPointerModel = axisModel.getModel('axisPointer', axisPointerParentModel);
      var labelModel = axisPointerModel.getModel('label');
      var lineColor = axisPointerModel.get('lineStyle.color');

      if (!labelModel.get('show') || !axisPointerModel.get('show')) {
        return;
      }

      var val = data[idx];
      var formatter = labelModel.get('formatter');
      var text = axis.scale.getLabel({
        value: val
      });

      if (formatter != null) {
        text = formatter(text, data);
      } else {
        if (axis.scale.type === 'interval' || axis.scale.type === 'log') {
          var precision = echarts.number.getPrecisionSafe(axis.scale.getTicks()[0]);
          text = val.toFixed(precision + 2);
        }
      }

      var labelColor = labelModel.get('color');
      var textEl = new echarts.graphic.Text({
        style: createTextStyle(labelModel, {
          text: text,
          fill: labelColor || lineColor,
          align: 'left',
          verticalAlign: 'top'
        })
      });
      var coords = axisPointerLabelsSurface.add(textEl);
      var rect = textEl.getBoundingRect();

      var dpr = this._api.getDevicePixelRatio();

      var pos = axisInfo.rootNode.position.toArray();
      var otherIdx = dimIndicesMap[otherDim[axisInfo.dim]];
      pos[otherIdx] += (axisInfo.flipped ? -1 : 1) * labelModel.get('margin');
      pos[dimIndicesMap[axisInfo.dim]] = axis.dataToCoord(data[idx]);
      axisPointerLabelsMesh.geometry.addSprite(pos, [rect.width * dpr, rect.height * dpr], coords, axisInfo.textAlign, axisInfo.textVerticalAlign);
    }, this);

    axisPointerLabelsSurface.getZr().refreshImmediately();
    axisPointerLabelsMesh.material.set('uvScale', axisPointerLabelsSurface.getCoordsScale());
    axisPointerLabelsMesh.geometry.convertToTypedArray();
  },
  dispose: function () {
    this.groupGL.removeAll();

    this._control.dispose();

    this._axisLabelSurface.dispose();

    this._axisPointerLabelsSurface.dispose();
  }
});