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