import * as echarts from 'echarts/lib/echarts';
import { getLayoutRect } from 'echarts/lib/util/layout';
import graphicGL from '../../util/graphicGL';
import ViewGL from '../../core/ViewGL';
import Lines2DGeometry from '../../util/geometry/Lines2D';
import retrieve from '../../util/retrieve';
import ForceAtlas2GPU from './ForceAtlas2GPU';
import ForceAtlas2 from './ForceAtlas2';
import requestAnimationFrame from 'zrender/lib/animation/requestAnimationFrame';
import glmatrix from 'claygl/src/dep/glmatrix';
import { getItemVisualColor, getItemVisualOpacity } from '../../util/visual';
var vec2 = glmatrix.vec2;
import Roam2DControl from '../../util/Roam2DControl';
import PointsBuilder from '../common/PointsBuilder';
import lines2DGLSL from '../../util/shader/lines2D.glsl.js';
graphicGL.Shader.import(lines2DGLSL);
var globalLayoutId = 1;
export default echarts.ChartView.extend({
  type: 'graphGL',
  __ecgl__: true,
  init: function (ecModel, api) {
    this.groupGL = new graphicGL.Node();
    this.viewGL = new ViewGL('orthographic');
    this.viewGL.camera.left = this.viewGL.camera.right = 0;
    this.viewGL.add(this.groupGL);
    this._pointsBuilder = new PointsBuilder(true, api); // Mesh used during force directed layout.

    this._forceEdgesMesh = new graphicGL.Mesh({
      material: new graphicGL.Material({
        shader: graphicGL.createShader('ecgl.forceAtlas2.edges'),
        transparent: true,
        depthMask: false,
        depthTest: false
      }),
      $ignorePicking: true,
      geometry: new graphicGL.Geometry({
        attributes: {
          node: new graphicGL.Geometry.Attribute('node', 'float', 2),
          color: new graphicGL.Geometry.Attribute('color', 'float', 4, 'COLOR')
        },
        dynamic: true,
        mainAttribute: 'node'
      }),
      renderOrder: -1,
      mode: graphicGL.Mesh.LINES
    }); // Mesh used after force directed layout.

    this._edgesMesh = new graphicGL.Mesh({
      material: new graphicGL.Material({
        shader: graphicGL.createShader('ecgl.meshLines2D'),
        transparent: true,
        depthMask: false,
        depthTest: false
      }),
      $ignorePicking: true,
      geometry: new Lines2DGeometry({
        useNativeLine: false,
        dynamic: true
      }),
      renderOrder: -1,
      culling: false
    });
    this._layoutId = 0;
    this._control = new Roam2DControl({
      zr: api.getZr(),
      viewGL: this.viewGL
    });

    this._control.setTarget(this.groupGL);

    this._control.init();

    this._clickHandler = this._clickHandler.bind(this);
  },
  render: function (seriesModel, ecModel, api) {
    this.groupGL.add(this._pointsBuilder.rootNode);
    this._model = seriesModel;
    this._api = api;

    this._initLayout(seriesModel, ecModel, api);

    this._pointsBuilder.update(seriesModel, ecModel, api);

    if (!(this._forceLayoutInstance instanceof ForceAtlas2GPU)) {
      this.groupGL.remove(this._forceEdgesMesh);
    }

    this._updateCamera(seriesModel, api);

    this._control.off('update');

    this._control.on('update', function () {
      api.dispatchAction({
        type: 'graphGLRoam',
        seriesId: seriesModel.id,
        zoom: this._control.getZoom(),
        offset: this._control.getOffset()
      });

      this._pointsBuilder.updateView(this.viewGL.camera);
    }, this);

    this._control.setZoom(retrieve.firstNotNull(seriesModel.get('zoom'), 1));

    this._control.setOffset(seriesModel.get('offset') || [0, 0]);

    var mesh = this._pointsBuilder.getPointsMesh();

    mesh.off('mousemove', this._mousemoveHandler);
    mesh.off('mouseout', this._mouseOutHandler, this);
    api.getZr().off('click', this._clickHandler);
    this._pointsBuilder.highlightOnMouseover = true;

    if (seriesModel.get('focusNodeAdjacency')) {
      var focusNodeAdjacencyOn = seriesModel.get('focusNodeAdjacencyOn');

      if (focusNodeAdjacencyOn === 'click') {
        // Remove default emphasis effect
        api.getZr().on('click', this._clickHandler);
      } else if (focusNodeAdjacencyOn === 'mouseover') {
        mesh.on('mousemove', this._mousemoveHandler, this);
        mesh.on('mouseout', this._mouseOutHandler, this);
        this._pointsBuilder.highlightOnMouseover = false;
      } else {
        if (process.env.NODE_ENV !== 'production') {
          console.warn('Unkown focusNodeAdjacencyOn value \s' + focusNodeAdjacencyOn);
        }
      }
    } // Reset


    this._lastMouseOverDataIndex = -1;
  },
  _clickHandler: function (e) {
    if (this._layouting) {
      return;
    }

    var dataIndex = this._pointsBuilder.getPointsMesh().dataIndex;

    if (dataIndex >= 0) {
      this._api.dispatchAction({
        type: 'graphGLFocusNodeAdjacency',
        seriesId: this._model.id,
        dataIndex: dataIndex
      });
    } else {
      this._api.dispatchAction({
        type: 'graphGLUnfocusNodeAdjacency',
        seriesId: this._model.id
      });
    }
  },
  _mousemoveHandler: function (e) {
    if (this._layouting) {
      return;
    }

    var dataIndex = this._pointsBuilder.getPointsMesh().dataIndex;

    if (dataIndex >= 0) {
      if (dataIndex !== this._lastMouseOverDataIndex) {
        this._api.dispatchAction({
          type: 'graphGLFocusNodeAdjacency',
          seriesId: this._model.id,
          dataIndex: dataIndex
        });
      }
    } else {
      this._mouseOutHandler(e);
    }

    this._lastMouseOverDataIndex = dataIndex;
  },
  _mouseOutHandler: function (e) {
    if (this._layouting) {
      return;
    }

    this._api.dispatchAction({
      type: 'graphGLUnfocusNodeAdjacency',
      seriesId: this._model.id
    });

    this._lastMouseOverDataIndex = -1;
  },
  _updateForceEdgesGeometry: function (edges, seriesModel) {
    var geometry = this._forceEdgesMesh.geometry;
    var edgeData = seriesModel.getEdgeData();
    var offset = 0;
    var layoutInstance = this._forceLayoutInstance;
    var vertexCount = edgeData.count() * 2;
    geometry.attributes.node.init(vertexCount);
    geometry.attributes.color.init(vertexCount);
    edgeData.each(function (idx) {
      var edge = edges[idx];
      geometry.attributes.node.set(offset, layoutInstance.getNodeUV(edge.node1));
      geometry.attributes.node.set(offset + 1, layoutInstance.getNodeUV(edge.node2));
      var color = getItemVisualColor(edgeData, edge.dataIndex);
      var colorArr = graphicGL.parseColor(color);
      colorArr[3] *= retrieve.firstNotNull(getItemVisualOpacity(edgeData, edge.dataIndex), 1);
      geometry.attributes.color.set(offset, colorArr);
      geometry.attributes.color.set(offset + 1, colorArr);
      offset += 2;
    });
    geometry.dirty();
  },
  _updateMeshLinesGeometry: function () {
    var edgeData = this._model.getEdgeData();

    var geometry = this._edgesMesh.geometry;

    var edgeData = this._model.getEdgeData();

    var points = this._model.getData().getLayout('points');

    geometry.resetOffset();
    geometry.setVertexCount(edgeData.count() * geometry.getLineVertexCount());
    geometry.setTriangleCount(edgeData.count() * geometry.getLineTriangleCount());
    var p0 = [];
    var p1 = [];
    var lineWidthQuery = ['lineStyle', 'width'];
    this._originalEdgeColors = new Float32Array(edgeData.count() * 4);
    this._edgeIndicesMap = new Float32Array(edgeData.count());
    edgeData.each(function (idx) {
      var edge = edgeData.graph.getEdgeByIndex(idx);
      var idx1 = edge.node1.dataIndex * 2;
      var idx2 = edge.node2.dataIndex * 2;
      p0[0] = points[idx1];
      p0[1] = points[idx1 + 1];
      p1[0] = points[idx2];
      p1[1] = points[idx2 + 1];
      var color = getItemVisualColor(edgeData, edge.dataIndex);
      var colorArr = graphicGL.parseColor(color);
      colorArr[3] *= retrieve.firstNotNull(getItemVisualOpacity(edgeData, edge.dataIndex), 1);
      var itemModel = edgeData.getItemModel(edge.dataIndex);

      var lineWidth = retrieve.firstNotNull(itemModel.get(lineWidthQuery), 1) * this._api.getDevicePixelRatio();

      geometry.addLine(p0, p1, colorArr, lineWidth);

      for (var k = 0; k < 4; k++) {
        this._originalEdgeColors[edge.dataIndex * 4 + k] = colorArr[k];
      }

      this._edgeIndicesMap[edge.dataIndex] = idx;
    }, this);
    geometry.dirty();
  },
  _updateForceNodesGeometry: function (nodeData) {
    var pointsMesh = this._pointsBuilder.getPointsMesh();

    var pos = [];

    for (var i = 0; i < nodeData.count(); i++) {
      this._forceLayoutInstance.getNodeUV(i, pos);

      pointsMesh.geometry.attributes.position.set(i, pos);
    }

    pointsMesh.geometry.dirty('position');
  },
  _initLayout: function (seriesModel, ecModel, api) {
    var layout = seriesModel.get('layout');
    var graph = seriesModel.getGraph();
    var boxLayoutOption = seriesModel.getBoxLayoutParams();
    var viewport = getLayoutRect(boxLayoutOption, {
      width: api.getWidth(),
      height: api.getHeight()
    });

    if (layout === 'force') {
      if (process.env.NODE_ENV !== 'production') {
        console.warn('Currently only forceAtlas2 layout supported.');
      }

      layout = 'forceAtlas2';
    } // Stop previous layout


    this.stopLayout(seriesModel, ecModel, api, {
      beforeLayout: true
    });
    var nodeData = seriesModel.getData();
    var edgeData = seriesModel.getData();

    if (layout === 'forceAtlas2') {
      var layoutModel = seriesModel.getModel('forceAtlas2');
      var layoutInstance = this._forceLayoutInstance;
      var nodes = [];
      var edges = [];
      var nodeDataExtent = nodeData.getDataExtent('value');
      var edgeDataExtent = edgeData.getDataExtent('value');
      var edgeWeightRange = retrieve.firstNotNull(layoutModel.get('edgeWeight'), 1.0);
      var nodeWeightRange = retrieve.firstNotNull(layoutModel.get('nodeWeight'), 1.0);

      if (typeof edgeWeightRange === 'number') {
        edgeWeightRange = [edgeWeightRange, edgeWeightRange];
      }

      if (typeof nodeWeightRange === 'number') {
        nodeWeightRange = [nodeWeightRange, nodeWeightRange];
      }

      var offset = 0;
      var nodesIndicesMap = {};
      var layoutPoints = new Float32Array(nodeData.count() * 2);
      graph.eachNode(function (node) {
        var dataIndex = node.dataIndex;
        var value = nodeData.get('value', dataIndex);
        var x;
        var y;

        if (nodeData.hasItemOption) {
          var itemModel = nodeData.getItemModel(dataIndex);
          x = itemModel.get('x');
          y = itemModel.get('y');
        }

        if (x == null) {
          // Random in rectangle
          x = viewport.x + Math.random() * viewport.width;
          y = viewport.y + Math.random() * viewport.height;
        }

        layoutPoints[offset * 2] = x;
        layoutPoints[offset * 2 + 1] = y;
        nodesIndicesMap[node.id] = offset++;
        var mass = echarts.number.linearMap(value, nodeDataExtent, nodeWeightRange);

        if (isNaN(mass)) {
          if (!isNaN(nodeWeightRange[0])) {
            mass = nodeWeightRange[0];
          } else {
            mass = 1;
          }
        }

        nodes.push({
          x: x,
          y: y,
          mass: mass,
          size: nodeData.getItemVisual(dataIndex, 'symbolSize')
        });
      });
      nodeData.setLayout('points', layoutPoints);
      graph.eachEdge(function (edge) {
        var dataIndex = edge.dataIndex;
        var value = nodeData.get('value', dataIndex);
        var weight = echarts.number.linearMap(value, edgeDataExtent, edgeWeightRange);

        if (isNaN(weight)) {
          if (!isNaN(edgeWeightRange[0])) {
            weight = edgeWeightRange[0];
          } else {
            weight = 1;
          }
        }

        edges.push({
          node1: nodesIndicesMap[edge.node1.id],
          node2: nodesIndicesMap[edge.node2.id],
          weight: weight,
          dataIndex: dataIndex
        });
      });

      if (!layoutInstance) {
        var isGPU = layoutModel.get('GPU');

        if (this._forceLayoutInstance) {
          if (isGPU && !(this._forceLayoutInstance instanceof ForceAtlas2GPU) || !isGPU && !(this._forceLayoutInstance instanceof ForceAtlas2)) {
            // Mark to dispose
            this._forceLayoutInstanceToDispose = this._forceLayoutInstance;
          }
        }

        layoutInstance = this._forceLayoutInstance = isGPU ? new ForceAtlas2GPU() : new ForceAtlas2();
      }

      layoutInstance.initData(nodes, edges);
      layoutInstance.updateOption(layoutModel.option); // Update lines geometry after first layout;

      this._updateForceEdgesGeometry(layoutInstance.getEdges(), seriesModel);

      this._updatePositionTexture();

      api.dispatchAction({
        type: 'graphGLStartLayout',
        from: this.uid
      });
    } else {
      var layoutPoints = new Float32Array(nodeData.count() * 2);
      var offset = 0;
      graph.eachNode(function (node) {
        var dataIndex = node.dataIndex;
        var x;
        var y;

        if (nodeData.hasItemOption) {
          var itemModel = nodeData.getItemModel(dataIndex);
          x = itemModel.get('x');
          y = itemModel.get('y');
        }

        layoutPoints[offset++] = x;
        layoutPoints[offset++] = y;
      });
      nodeData.setLayout('points', layoutPoints);

      this._updateAfterLayout(seriesModel, ecModel, api);
    }
  },
  _updatePositionTexture: function () {
    var positionTex = this._forceLayoutInstance.getNodePositionTexture();

    this._pointsBuilder.setPositionTexture(positionTex);

    this._forceEdgesMesh.material.set('positionTex', positionTex);
  },
  startLayout: function (seriesModel, ecModel, api, payload) {
    if (payload && payload.from != null && payload.from !== this.uid) {
      return;
    }

    var viewGL = this.viewGL;
    var api = this._api;
    var layoutInstance = this._forceLayoutInstance;

    var data = this._model.getData();

    var layoutModel = this._model.getModel('forceAtlas2');

    if (!layoutInstance) {
      if (process.env.NODE_ENV !== 'production') {
        console.error('None layout don\'t have startLayout action');
      }

      return;
    }

    this.groupGL.remove(this._edgesMesh);
    this.groupGL.add(this._forceEdgesMesh);

    if (!this._forceLayoutInstance) {
      return;
    }

    this._updateForceNodesGeometry(seriesModel.getData());

    this._pointsBuilder.hideLabels();

    var self = this;
    var layoutId = this._layoutId = globalLayoutId++;
    var maxSteps = layoutModel.getShallow('maxSteps');
    var steps = layoutModel.getShallow('steps');
    var stepsCount = 0;
    var syncStepCount = Math.max(steps * 2, 20);

    var doLayout = function (layoutId) {
      if (layoutId !== self._layoutId) {
        return;
      }

      if (layoutInstance.isFinished(maxSteps)) {
        api.dispatchAction({
          type: 'graphGLStopLayout',
          from: self.uid
        });
        api.dispatchAction({
          type: 'graphGLFinishLayout',
          points: data.getLayout('points'),
          from: self.uid
        });
        return;
      }

      layoutInstance.update(viewGL.layer.renderer, steps, function () {
        self._updatePositionTexture(); // PENDING Performance.


        stepsCount += steps; // Sync posiiton every 20 steps.

        if (stepsCount >= syncStepCount) {
          self._syncNodePosition(seriesModel);

          stepsCount = 0;
        } // Position texture will been swapped. set every time.


        api.getZr().refresh();
        requestAnimationFrame(function () {
          doLayout(layoutId);
        });
      });
    };

    requestAnimationFrame(function () {
      if (self._forceLayoutInstanceToDispose) {
        self._forceLayoutInstanceToDispose.dispose(viewGL.layer.renderer);

        self._forceLayoutInstanceToDispose = null;
      }

      doLayout(layoutId);
    });
    this._layouting = true;
  },
  stopLayout: function (seriesModel, ecModel, api, payload) {
    if (payload && payload.from != null && payload.from !== this.uid) {
      return;
    }

    this._layoutId = 0;
    this.groupGL.remove(this._forceEdgesMesh);
    this.groupGL.add(this._edgesMesh);

    if (!this._forceLayoutInstance) {
      return;
    }

    if (!this.viewGL.layer) {
      return;
    }

    if (!(payload && payload.beforeLayout)) {
      this._syncNodePosition(seriesModel);

      this._updateAfterLayout(seriesModel, ecModel, api);
    }

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

    this._layouting = false;
  },
  _syncNodePosition: function (seriesModel) {
    var points = this._forceLayoutInstance.getNodePosition(this.viewGL.layer.renderer);

    seriesModel.getData().setLayout('points', points);
    seriesModel.setNodePosition(points);
  },
  _updateAfterLayout: function (seriesModel, ecModel, api) {
    this._updateMeshLinesGeometry();

    this._pointsBuilder.removePositionTexture();

    this._pointsBuilder.updateLayout(seriesModel, ecModel, api);

    this._pointsBuilder.updateView(this.viewGL.camera);

    this._pointsBuilder.updateLabels();

    this._pointsBuilder.showLabels();
  },
  focusNodeAdjacency: function (seriesModel, ecModel, api, payload) {
    var data = this._model.getData();

    this._downplayAll();

    var dataIndex = payload.dataIndex;
    var graph = data.graph;
    var focusNodes = [];
    var node = graph.getNodeByIndex(dataIndex);
    focusNodes.push(node);
    node.edges.forEach(function (edge) {
      if (edge.dataIndex < 0) {
        return;
      }

      edge.node1 !== node && focusNodes.push(edge.node1);
      edge.node2 !== node && focusNodes.push(edge.node2);
    }, this);

    this._pointsBuilder.fadeOutAll(0.05);

    this._fadeOutEdgesAll(0.05);

    focusNodes.forEach(function (node) {
      this._pointsBuilder.highlight(data, node.dataIndex);
    }, this);

    this._pointsBuilder.updateLabels(focusNodes.map(function (node) {
      return node.dataIndex;
    }));

    var focusEdges = [];
    node.edges.forEach(function (edge) {
      if (edge.dataIndex >= 0) {
        this._highlightEdge(edge.dataIndex);

        focusEdges.push(edge);
      }
    }, this);
    this._focusNodes = focusNodes;
    this._focusEdges = focusEdges;
  },
  unfocusNodeAdjacency: function (seriesModel, ecModel, api, payload) {
    this._downplayAll();

    this._pointsBuilder.fadeInAll();

    this._fadeInEdgesAll();

    this._pointsBuilder.updateLabels();
  },
  _highlightEdge: function (dataIndex) {
    var itemModel = this._model.getEdgeData().getItemModel(dataIndex);

    var emphasisColor = graphicGL.parseColor(itemModel.get('emphasis.lineStyle.color') || itemModel.get('lineStyle.color'));
    var emphasisOpacity = retrieve.firstNotNull(itemModel.get('emphasis.lineStyle.opacity'), itemModel.get('lineStyle.opacity'), 1);
    emphasisColor[3] *= emphasisOpacity;

    this._edgesMesh.geometry.setItemColor(this._edgeIndicesMap[dataIndex], emphasisColor);
  },
  _downplayAll: function () {
    if (this._focusNodes) {
      this._focusNodes.forEach(function (node) {
        this._pointsBuilder.downplay(this._model.getData(), node.dataIndex);
      }, this);
    }

    if (this._focusEdges) {
      this._focusEdges.forEach(function (edge) {
        this._downplayEdge(edge.dataIndex);
      }, this);
    }
  },
  _downplayEdge: function (dataIndex) {
    var color = this._getColor(dataIndex, []);

    this._edgesMesh.geometry.setItemColor(this._edgeIndicesMap[dataIndex], color);
  },
  _setEdgeFade: function () {
    var color = [];
    return function (dataIndex, percent) {
      this._getColor(dataIndex, color);

      color[3] *= percent;

      this._edgesMesh.geometry.setItemColor(this._edgeIndicesMap[dataIndex], color);
    };
  }(),
  _getColor: function (dataIndex, out) {
    for (var i = 0; i < 4; i++) {
      out[i] = this._originalEdgeColors[dataIndex * 4 + i];
    }

    return out;
  },
  _fadeOutEdgesAll: function (percent) {
    var graph = this._model.getData().graph;

    graph.eachEdge(function (edge) {
      this._setEdgeFade(edge.dataIndex, percent);
    }, this);
  },
  _fadeInEdgesAll: function () {
    this._fadeOutEdgesAll(1);
  },
  _updateCamera: function (seriesModel, api) {
    this.viewGL.setViewport(0, 0, api.getWidth(), api.getHeight(), api.getDevicePixelRatio());
    var camera = this.viewGL.camera;
    var nodeData = seriesModel.getData();
    var points = nodeData.getLayout('points');
    var min = vec2.create(Infinity, Infinity);
    var max = vec2.create(-Infinity, -Infinity);
    var pt = [];

    for (var i = 0; i < points.length;) {
      pt[0] = points[i++];
      pt[1] = points[i++];
      vec2.min(min, min, pt);
      vec2.max(max, max, pt);
    }

    var cy = (max[1] + min[1]) / 2;
    var cx = (max[0] + min[0]) / 2; // Only fit the camera when graph is not in the center.
    // PENDING

    if (cx > camera.left && cx < camera.right && cy < camera.bottom && cy > camera.top) {
      return;
    } // Scale a bit


    var width = Math.max(max[0] - min[0], 10); // Keep aspect

    var height = width / api.getWidth() * api.getHeight();
    width *= 1.4;
    height *= 1.4;
    min[0] -= width * 0.2;
    camera.left = min[0];
    camera.top = cy - height / 2;
    camera.bottom = cy + height / 2;
    camera.right = width + min[0];
    camera.near = 0;
    camera.far = 100;
  },
  dispose: function () {
    var renderer = this.viewGL.layer.renderer;

    if (this._forceLayoutInstance) {
      this._forceLayoutInstance.dispose(renderer);
    }

    this.groupGL.removeAll(); // Stop layout.

    this._layoutId = -1;

    this._pointsBuilder.dispose();
  },
  remove: function () {
    this.groupGL.removeAll();

    this._control.dispose();
  }
});