import * as echarts from 'echarts/lib/echarts';
import graphicGL from '../../util/graphicGL';
import glmatrix from 'claygl/src/dep/glmatrix';
import trianglesSortMixin from '../../util/geometry/trianglesSortMixin';
import { getItemVisualColor, getItemVisualOpacity } from '../../util/visual';
var vec3 = glmatrix.vec3;

function isPointsNaN(pt) {
  return isNaN(pt[0]) || isNaN(pt[1]) || isNaN(pt[2]);
}

export default echarts.ChartView.extend({
  type: 'surface',
  __ecgl__: true,
  init: function (ecModel, api) {
    this.groupGL = new graphicGL.Node();
  },
  render: function (seriesModel, ecModel, api) {
    // Swap surfaceMesh
    var tmp = this._prevSurfaceMesh;
    this._prevSurfaceMesh = this._surfaceMesh;
    this._surfaceMesh = tmp;

    if (!this._surfaceMesh) {
      this._surfaceMesh = this._createSurfaceMesh();
    }

    this.groupGL.remove(this._prevSurfaceMesh);
    this.groupGL.add(this._surfaceMesh);
    var coordSys = seriesModel.coordinateSystem;
    var shading = seriesModel.get('shading');
    var data = seriesModel.getData();
    var shadingPrefix = 'ecgl.' + shading;

    if (!this._surfaceMesh.material || this._surfaceMesh.material.shader.name !== shadingPrefix) {
      this._surfaceMesh.material = graphicGL.createMaterial(shadingPrefix, ['VERTEX_COLOR', 'DOUBLE_SIDED']);
    }

    graphicGL.setMaterialFromModel(shading, this._surfaceMesh.material, seriesModel, api);

    if (coordSys && coordSys.viewGL) {
      coordSys.viewGL.add(this.groupGL);
      var methodName = coordSys.viewGL.isLinearSpace() ? 'define' : 'undefine';

      this._surfaceMesh.material[methodName]('fragment', 'SRGB_DECODE');
    }

    var isParametric = seriesModel.get('parametric');
    var dataShape = seriesModel.get('dataShape');

    if (!dataShape) {
      dataShape = this._getDataShape(data, isParametric);

      if (process.env.NODE_ENV !== 'production') {
        if (seriesModel.get('data')) {
          console.warn('dataShape is not provided. Guess it is ', dataShape);
        }
      }
    }

    var wireframeModel = seriesModel.getModel('wireframe');
    var wireframeLineWidth = wireframeModel.get('lineStyle.width');
    var showWireframe = wireframeModel.get('show') && wireframeLineWidth > 0;

    this._updateSurfaceMesh(this._surfaceMesh, seriesModel, dataShape, showWireframe);

    var material = this._surfaceMesh.material;

    if (showWireframe) {
      material.define('WIREFRAME_QUAD');
      material.set('wireframeLineWidth', wireframeLineWidth);
      material.set('wireframeLineColor', graphicGL.parseColor(wireframeModel.get('lineStyle.color')));
    } else {
      material.undefine('WIREFRAME_QUAD');
    }

    this._initHandler(seriesModel, api);

    this._updateAnimation(seriesModel);
  },
  _updateAnimation: function (seriesModel) {
    graphicGL.updateVertexAnimation([['prevPosition', 'position'], ['prevNormal', 'normal']], this._prevSurfaceMesh, this._surfaceMesh, seriesModel);
  },
  _createSurfaceMesh: function () {
    var mesh = new graphicGL.Mesh({
      geometry: new graphicGL.Geometry({
        dynamic: true,
        sortTriangles: true
      }),
      shadowDepthMaterial: new graphicGL.Material({
        shader: new graphicGL.Shader(graphicGL.Shader.source('ecgl.sm.depth.vertex'), graphicGL.Shader.source('ecgl.sm.depth.fragment'))
      }),
      culling: false,
      // Render after axes
      renderOrder: 10,
      // Render normal in normal pass
      renderNormal: true
    });
    mesh.geometry.createAttribute('barycentric', 'float', 4);
    mesh.geometry.createAttribute('prevPosition', 'float', 3);
    mesh.geometry.createAttribute('prevNormal', 'float', 3);
    Object.assign(mesh.geometry, trianglesSortMixin);
    return mesh;
  },
  _initHandler: function (seriesModel, api) {
    var data = seriesModel.getData();
    var surfaceMesh = this._surfaceMesh;
    var coordSys = seriesModel.coordinateSystem;

    function getNearestPointIdx(triangle, point) {
      var nearestDist = Infinity;
      var nearestIdx = -1;
      var pos = [];

      for (var i = 0; i < triangle.length; i++) {
        surfaceMesh.geometry.attributes.position.get(triangle[i], pos);
        var dist = vec3.dist(point.array, pos);

        if (dist < nearestDist) {
          nearestDist = dist;
          nearestIdx = triangle[i];
        }
      }

      return nearestIdx;
    }

    surfaceMesh.seriesIndex = seriesModel.seriesIndex;
    var lastDataIndex = -1;
    surfaceMesh.off('mousemove');
    surfaceMesh.off('mouseout');
    surfaceMesh.on('mousemove', function (e) {
      var idx = getNearestPointIdx(e.triangle, e.point);

      if (idx >= 0) {
        var point = [];
        surfaceMesh.geometry.attributes.position.get(idx, point);
        var value = coordSys.pointToData(point);
        var minDist = Infinity;
        var dataIndex = -1;
        var item = [];

        for (var i = 0; i < data.count(); i++) {
          item[0] = data.get('x', i);
          item[1] = data.get('y', i);
          item[2] = data.get('z', i);
          var dist = vec3.squaredDistance(item, value);

          if (dist < minDist) {
            dataIndex = i;
            minDist = dist;
          }
        }

        if (dataIndex !== lastDataIndex) {
          api.dispatchAction({
            type: 'grid3DShowAxisPointer',
            value: value
          });
        }

        lastDataIndex = dataIndex;
        surfaceMesh.dataIndex = dataIndex;
      } else {
        surfaceMesh.dataIndex = -1;
      }
    }, this);
    surfaceMesh.on('mouseout', function (e) {
      lastDataIndex = -1;
      surfaceMesh.dataIndex = -1;
      api.dispatchAction({
        type: 'grid3DHideAxisPointer'
      });
    }, this);
  },
  _updateSurfaceMesh: function (surfaceMesh, seriesModel, dataShape, showWireframe) {
    var geometry = surfaceMesh.geometry;
    var data = seriesModel.getData();
    var pointsArr = data.getLayout('points');
    var invalidDataCount = 0;
    data.each(function (idx) {
      if (!data.hasValue(idx)) {
        invalidDataCount++;
      }
    });
    var needsSplitQuad = invalidDataCount || showWireframe;
    var positionAttr = geometry.attributes.position;
    var normalAttr = geometry.attributes.normal;
    var texcoordAttr = geometry.attributes.texcoord0;
    var barycentricAttr = geometry.attributes.barycentric;
    var colorAttr = geometry.attributes.color;
    var row = dataShape[0];
    var column = dataShape[1];
    var shading = seriesModel.get('shading');
    var needsNormal = shading !== 'color';

    if (needsSplitQuad) {
      // TODO, If needs remove the invalid points, or set color transparent.
      var vertexCount = (row - 1) * (column - 1) * 4;
      positionAttr.init(vertexCount);

      if (showWireframe) {
        barycentricAttr.init(vertexCount);
      }
    } else {
      positionAttr.value = new Float32Array(pointsArr);
    }

    colorAttr.init(geometry.vertexCount);
    texcoordAttr.init(geometry.vertexCount);
    var quadToTriangle = [0, 3, 1, 1, 3, 2]; // 3----2
    // 0----1
    // Make sure pixels on 1---3 edge will not have channel 0.
    // And pixels on four edges have at least one channel 0.

    var quadBarycentric = [[1, 1, 0, 0], [0, 1, 0, 1], [1, 0, 0, 1], [1, 0, 1, 0]];
    var indices = geometry.indices = new (geometry.vertexCount > 0xffff ? Uint32Array : Uint16Array)((row - 1) * (column - 1) * 6);

    var getQuadIndices = function (i, j, out) {
      out[1] = i * column + j;
      out[0] = i * column + j + 1;
      out[3] = (i + 1) * column + j + 1;
      out[2] = (i + 1) * column + j;
    };

    var isTransparent = false;

    if (needsSplitQuad) {
      var quadIndices = [];
      var pos = [];
      var faceOffset = 0;

      if (needsNormal) {
        normalAttr.init(geometry.vertexCount);
      } else {
        normalAttr.value = null;
      }

      var pts = [[], [], []];
      var v21 = [],
          v32 = [];
      var normal = vec3.create();

      var getFromArray = function (arr, idx, out) {
        var idx3 = idx * 3;
        out[0] = arr[idx3];
        out[1] = arr[idx3 + 1];
        out[2] = arr[idx3 + 2];
        return out;
      };

      var vertexNormals = new Float32Array(pointsArr.length);
      var vertexColors = new Float32Array(pointsArr.length / 3 * 4);

      for (var i = 0; i < data.count(); i++) {
        if (data.hasValue(i)) {
          var rgbaArr = graphicGL.parseColor(getItemVisualColor(data, i));
          var opacity = getItemVisualOpacity(data, i);
          opacity != null && (rgbaArr[3] *= opacity);

          if (rgbaArr[3] < 0.99) {
            isTransparent = true;
          }

          for (var k = 0; k < 4; k++) {
            vertexColors[i * 4 + k] = rgbaArr[k];
          }
        }
      }

      var farPoints = [1e7, 1e7, 1e7];

      for (var i = 0; i < row - 1; i++) {
        for (var j = 0; j < column - 1; j++) {
          var dataIndex = i * (column - 1) + j;
          var vertexOffset = dataIndex * 4;
          getQuadIndices(i, j, quadIndices);
          var invisibleQuad = false;

          for (var k = 0; k < 4; k++) {
            getFromArray(pointsArr, quadIndices[k], pos);

            if (isPointsNaN(pos)) {
              // Quad is invisible if any point is NaN
              invisibleQuad = true;
            }
          }

          for (var k = 0; k < 4; k++) {
            if (invisibleQuad) {
              // Move point far away
              positionAttr.set(vertexOffset + k, farPoints);
            } else {
              getFromArray(pointsArr, quadIndices[k], pos);
              positionAttr.set(vertexOffset + k, pos);
            }

            if (showWireframe) {
              barycentricAttr.set(vertexOffset + k, quadBarycentric[k]);
            }
          }

          for (var k = 0; k < 6; k++) {
            indices[faceOffset++] = quadToTriangle[k] + vertexOffset;
          } // Vertex normals


          if (needsNormal && !invisibleQuad) {
            for (var k = 0; k < 2; k++) {
              var k3 = k * 3;

              for (var m = 0; m < 3; m++) {
                var idx = quadIndices[quadToTriangle[k3] + m];
                getFromArray(pointsArr, idx, pts[m]);
              }

              vec3.sub(v21, pts[0], pts[1]);
              vec3.sub(v32, pts[1], pts[2]);
              vec3.cross(normal, v21, v32); // Weighted by the triangle area

              for (var m = 0; m < 3; m++) {
                var idx3 = quadIndices[quadToTriangle[k3] + m] * 3;
                vertexNormals[idx3] = vertexNormals[idx3] + normal[0];
                vertexNormals[idx3 + 1] = vertexNormals[idx3 + 1] + normal[1];
                vertexNormals[idx3 + 2] = vertexNormals[idx3 + 2] + normal[2];
              }
            }
          }
        }
      }

      if (needsNormal) {
        for (var i = 0; i < vertexNormals.length / 3; i++) {
          getFromArray(vertexNormals, i, normal);
          vec3.normalize(normal, normal);
          vertexNormals[i * 3] = normal[0];
          vertexNormals[i * 3 + 1] = normal[1];
          vertexNormals[i * 3 + 2] = normal[2];
        }
      } // Split normal and colors, write to the attributes.


      var rgbaArr = [];
      var uvArr = [];

      for (var i = 0; i < row - 1; i++) {
        for (var j = 0; j < column - 1; j++) {
          var dataIndex = i * (column - 1) + j;
          var vertexOffset = dataIndex * 4;
          getQuadIndices(i, j, quadIndices);

          for (var k = 0; k < 4; k++) {
            for (var m = 0; m < 4; m++) {
              rgbaArr[m] = vertexColors[quadIndices[k] * 4 + m];
            }

            colorAttr.set(vertexOffset + k, rgbaArr);

            if (needsNormal) {
              getFromArray(vertexNormals, quadIndices[k], normal);
              normalAttr.set(vertexOffset + k, normal);
            }

            var idx = quadIndices[k];
            uvArr[0] = idx % column / (column - 1);
            uvArr[1] = Math.floor(idx / column) / (row - 1);
            texcoordAttr.set(vertexOffset + k, uvArr);
          }

          dataIndex++;
        }
      }
    } else {
      var uvArr = [];

      for (var i = 0; i < data.count(); i++) {
        uvArr[0] = i % column / (column - 1);
        uvArr[1] = Math.floor(i / column) / (row - 1);
        var rgbaArr = graphicGL.parseColor(getItemVisualColor(data, i));
        var opacity = getItemVisualOpacity(data, i);
        opacity != null && (rgbaArr[3] *= opacity);

        if (rgbaArr[3] < 0.99) {
          isTransparent = true;
        }

        colorAttr.set(i, rgbaArr);
        texcoordAttr.set(i, uvArr);
      }

      var quadIndices = []; // Triangles

      var cursor = 0;

      for (var i = 0; i < row - 1; i++) {
        for (var j = 0; j < column - 1; j++) {
          getQuadIndices(i, j, quadIndices);

          for (var k = 0; k < 6; k++) {
            indices[cursor++] = quadIndices[quadToTriangle[k]];
          }
        }
      }

      if (needsNormal) {
        geometry.generateVertexNormals();
      } else {
        normalAttr.value = null;
      }
    }

    if (surfaceMesh.material.get('normalMap')) {
      geometry.generateTangents();
    }

    geometry.updateBoundingBox();
    geometry.dirty();
    surfaceMesh.material.transparent = isTransparent;
    surfaceMesh.material.depthMask = !isTransparent;
  },
  _getDataShape: function (data, isParametric) {
    var prevX = -Infinity;
    var rowCount = 0;
    var columnCount = 0;
    var prevColumnCount = 0;
    var mayInvalid = false;
    var rowDim = isParametric ? 'u' : 'x';
    var dataCount = data.count(); // Check data format

    for (var i = 0; i < dataCount; i++) {
      var x = data.get(rowDim, i);

      if (x < prevX) {
        if (prevColumnCount && prevColumnCount !== columnCount) {
          if (process.env.NODE_ENV !== 'production') {
            mayInvalid = true;
          }
        } // A new row.


        prevColumnCount = columnCount;
        columnCount = 0;
        rowCount++;
      }

      prevX = x;
      columnCount++;
    }

    if (!rowCount || columnCount === 1) {
      mayInvalid = true;
    }

    if (!mayInvalid) {
      return [rowCount + 1, columnCount];
    }

    var rows = Math.floor(Math.sqrt(dataCount));

    while (rows > 0) {
      if (Math.floor(dataCount / rows) === dataCount / rows) {
        // Can be divided
        return [rows, dataCount / rows];
      }

      rows--;
    } // Bailout


    rows = Math.floor(Math.sqrt(dataCount));
    return [rows, rows];
  },
  dispose: function () {
    this.groupGL.removeAll();
  },
  remove: function () {
    this.groupGL.removeAll();
  }
});