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();
    }
});