/**
 * Provide orbit control for 3D objects
 *
 * @module echarts-gl/util/OrbitControl
 * @author Yi Shen(http://github.com/pissang)
 */

// TODO Remove magic numbers on sensitivity
import Base from 'claygl/src/core/Base';
import Vector2 from 'claygl/src/math/Vector2';
import Vector3 from 'claygl/src/math/Vector3';
import Quaternion from 'claygl/src/math/Quaternion';
import retrieve from './retrieve';
var firstNotNull = retrieve.firstNotNull;


var MOUSE_BUTTON_KEY_MAP = {
    left: 0,
    middle: 1,
    right: 2
};

function convertToArray(val) {
    if (!(val instanceof Array)) {
        val = [val, val];
    }
    return val;
}

/**
 * @alias module:echarts-x/util/OrbitControl
 */
var OrbitControl = Base.extend(function () {

    return {
        /**
         * @type {module:zrender~ZRender}
         */
        zr: null,

        /**
         * @type {module:echarts-gl/core/ViewGL}
         */
        viewGL: null,

        /**
         * @type {clay.math.Vector3}
         */
        _center: new Vector3(),

        /**
         * Minimum distance to the center
         * Only available when camera is perspective.
         * @type {number}
         * @default 0.5
         */
        minDistance: 0.5,

        /**
         * Maximum distance to the center
         * Only available when camera is perspective.
         * @type {number}
         * @default 2
         */
        maxDistance: 1.5,

        /**
         * Only available when camera is orthographic
         */
        maxOrthographicSize: 300,

        /**
         * Only available when camera is orthographic
         */
        minOrthographicSize: 30,

        /**
         * Minimum alpha rotation
         */
        minAlpha: -90,

        /**
         * Maximum alpha rotation
         */
        maxAlpha: 90,

        /**
         * Minimum beta rotation
         */
        minBeta: -Infinity,
        /**
         * Maximum beta rotation
         */
        maxBeta: Infinity,

        /**
         * Start auto rotating after still for the given time
         */
        autoRotateAfterStill: 0,

        /**
         * Direction of autoRotate. cw or ccw when looking top down.
         */
        autoRotateDirection: 'cw',

        /**
         * Degree per second
         */
        autoRotateSpeed: 60,

        /**
         * @param {number}
         */
        damping: 0.8,

        /**
         * @param {number}
         */
        rotateSensitivity: 1,

        /**
         * @param {number}
         */
        zoomSensitivity: 1,

        /**
         * @param {number}
         */
        panSensitivity: 1,

        panMouseButton: 'middle',
        rotateMouseButton: 'left',

        /**
         * Pan or rotate
         * @private
         * @type {String}
         */
        _mode: 'rotate',

        /**
         * @private
         * @type {clay.Camera}
         */
        _camera: null,

        _needsUpdate: false,

        _rotating: false,

        // Rotation around yAxis in radian
        _phi: 0,
        // Rotation around xAxis in radian
        _theta: 0,

        _mouseX: 0,
        _mouseY: 0,

        _rotateVelocity: new Vector2(),

        _panVelocity: new Vector2(),

        _distance: 500,

        _zoomSpeed: 0,

        _stillTimeout: 0,

        _animators: []
    };
}, function () {
    // Each OrbitControl has it's own handler
    ['_mouseDownHandler', '_mouseWheelHandler', '_mouseMoveHandler', '_mouseUpHandler',
    '_pinchHandler', '_contextMenuHandler', '_update'].forEach(function (hdlName) {
        this[hdlName] = this[hdlName].bind(this);
    }, this);
}, {
    /**
     * Initialize.
     * Mouse event binding
     */
    init: function () {
        var zr = this.zr;

        if (zr) {
            zr.on('mousedown', this._mouseDownHandler);
            zr.on('globalout', this._mouseUpHandler);
            zr.on('mousewheel', this._mouseWheelHandler);
            zr.on('pinch', this._pinchHandler);

            zr.animation.on('frame', this._update);

            zr.dom.addEventListener('contextmenu', this._contextMenuHandler);
        }
    },

    /**
     * Dispose.
     * Mouse event unbinding
     */
    dispose: function () {
        var zr = this.zr;

        if (zr) {
            zr.off('mousedown', this._mouseDownHandler);
            zr.off('mousemove', this._mouseMoveHandler);
            zr.off('mouseup', this._mouseUpHandler);
            zr.off('mousewheel', this._mouseWheelHandler);
            zr.off('pinch', this._pinchHandler);
            zr.off('globalout', this._mouseUpHandler);
            zr.dom.removeEventListener('contextmenu', this._contextMenuHandler);

            zr.animation.off('frame', this._update);
        }
        this.stopAllAnimation();
    },

    /**
     * Get distance
     * @return {number}
     */
    getDistance: function () {
        return this._distance;
    },

    /**
     * Set distance
     * @param {number} distance
     */
    setDistance: function (distance) {
        this._distance = distance;
        this._needsUpdate = true;
    },

    /**
     * Get size of orthographic viewing volume
     * @return {number}
     */
    getOrthographicSize: function () {
        return this._orthoSize;
    },

    /**
     * Set size of orthographic viewing volume
     * @param {number} size
     */
    setOrthographicSize: function (size) {
        this._orthoSize = size;
        this._needsUpdate = true;
    },

    /**
     * Get alpha rotation
     * Alpha angle for top-down rotation. Positive to rotate to top.
     *
     * Which means camera rotation around x axis.
     */
    getAlpha: function () {
        return this._theta / Math.PI * 180;
    },

    /**
     * Get beta rotation
     * Beta angle for left-right rotation. Positive to rotate to right.
     *
     * Which means camera rotation around y axis.
     */
    getBeta: function () {
        return -this._phi / Math.PI * 180;
    },

    /**
     * Get control center
     * @return {Array.<number>}
     */
    getCenter: function () {
        return this._center.toArray();
    },

    /**
     * Set alpha rotation angle
     * @param {number} alpha
     */
    setAlpha: function (alpha) {
        alpha = Math.max(Math.min(this.maxAlpha, alpha), this.minAlpha);

        this._theta = alpha / 180 * Math.PI;
        this._needsUpdate = true;
    },

    /**
     * Set beta rotation angle
     * @param {number} beta
     */
    setBeta: function (beta) {
        beta = Math.max(Math.min(this.maxBeta, beta), this.minBeta);

        this._phi = -beta / 180 * Math.PI;
        this._needsUpdate = true;
    },

    /**
     * Set control center
     * @param {Array.<number>} center
     */
    setCenter: function (centerArr) {
        this._center.setArray(centerArr);
    },

    /**
     * @param {module:echarts-gl/core/ViewGL} viewGL
     */
    setViewGL: function (viewGL) {
        this.viewGL = viewGL;
    },

    /**
     * @return {clay.Camera}
     */
    getCamera: function () {
        return this.viewGL.camera;
    },

    setFromViewControlModel: function (viewControlModel, extraOpts) {
        extraOpts = extraOpts || {};
        var baseDistance = extraOpts.baseDistance || 0;
        var baseOrthoSize = extraOpts.baseOrthoSize || 1;

        var projection = viewControlModel.get('projection');
        if (projection !== 'perspective' && projection !== 'orthographic' && projection !== 'isometric') {
            if (process.env.NODE_ENV !== 'production') {
                console.error('Unkown projection type %s, use perspective projection instead.', projection);
            }
            projection = 'perspective';
        }
        this._projection = projection;
        this.viewGL.setProjection(projection);

        var targetDistance = viewControlModel.get('distance') + baseDistance;
        var targetOrthographicSize = viewControlModel.get('orthographicSize') + baseOrthoSize;

        [
            ['damping', 0.8],
            ['autoRotate', false],
            ['autoRotateAfterStill', 3],
            ['autoRotateDirection', 'cw'],
            ['autoRotateSpeed', 10],
            ['minDistance', 30],
            ['maxDistance', 400],
            ['minOrthographicSize', 30],
            ['maxOrthographicSize', 300],
            ['minAlpha', -90],
            ['maxAlpha', 90],
            ['minBeta', -Infinity],
            ['maxBeta', Infinity],
            ['rotateSensitivity', 1],
            ['zoomSensitivity', 1],
            ['panSensitivity', 1],
            ['panMouseButton', 'left'],
            ['rotateMouseButton', 'middle'],
        ].forEach(function (prop) {
            this[prop[0]] = firstNotNull(viewControlModel.get(prop[0]), prop[1]);
        }, this);

        this.minDistance += baseDistance;
        this.maxDistance += baseDistance;
        this.minOrthographicSize += baseOrthoSize,
        this.maxOrthographicSize += baseOrthoSize;

        var ecModel = viewControlModel.ecModel;

        var animationOpts = {};
        ['animation', 'animationDurationUpdate', 'animationEasingUpdate'].forEach(function (key) {
            animationOpts[key] = firstNotNull(
                viewControlModel.get(key), ecModel && ecModel.get(key)
            );
        });

        var alpha = firstNotNull(extraOpts.alpha, viewControlModel.get('alpha')) || 0;
        var beta = firstNotNull(extraOpts.beta, viewControlModel.get('beta')) || 0;
        var center = firstNotNull(extraOpts.center, viewControlModel.get('center')) || [0, 0, 0];
        if (animationOpts.animation && animationOpts.animationDurationUpdate > 0 && this._notFirst) {
            this.animateTo({
                alpha: alpha,
                beta: beta,
                center: center,
                distance: targetDistance,
                orthographicSize: targetOrthographicSize,
                easing: animationOpts.animationEasingUpdate,
                duration: animationOpts.animationDurationUpdate
            });
        }
        else {
            this.setDistance(targetDistance);
            this.setAlpha(alpha);
            this.setBeta(beta);
            this.setCenter(center);
            this.setOrthographicSize(targetOrthographicSize);
        }

        this._notFirst = true;

        this._validateProperties();
    },

    _validateProperties: function () {
        if (process.env.NODE_ENV !== 'production') {
            if (MOUSE_BUTTON_KEY_MAP[this.panMouseButton] == null) {
                console.error('Unkown panMouseButton %s. It should be left|middle|right', this.panMouseButton);
            }
            if (MOUSE_BUTTON_KEY_MAP[this.rotateMouseButton] == null) {
                console.error('Unkown rotateMouseButton %s. It should be left|middle|right', this.rotateMouseButton);
            }
            if (this.autoRotateDirection !== 'cw' && this.autoRotateDirection !== 'ccw') {
                console.error('Unkown autoRotateDirection %s. It should be cw|ccw', this.autoRotateDirection);
            }
        }
    },

    /**
     * @param {Object} opts
     * @param {number} opts.distance
     * @param {number} opts.alpha
     * @param {number} opts.beta
     * @param {number} opts.orthographicSize
     * @param {number} [opts.duration=1000]
     * @param {number} [opts.easing='linear']
     */
    animateTo: function (opts) {
        var zr = this.zr;
        var self = this;

        var obj = {};
        var target = {};

        if (opts.distance != null) {
            obj.distance = this.getDistance();
            target.distance = opts.distance;
        }
        if (opts.orthographicSize != null) {
            obj.orthographicSize = this.getOrthographicSize();
            target.orthographicSize = opts.orthographicSize;
        }
        if (opts.alpha != null) {
            obj.alpha = this.getAlpha();
            target.alpha = opts.alpha;
        }
        if (opts.beta != null) {
            obj.beta = this.getBeta();
            target.beta = opts.beta;
        }
        if (opts.center != null) {
            obj.center = this.getCenter();
            target.center = opts.center;
        }

        return this._addAnimator(
            zr.animation.animate(obj)
                .when(opts.duration || 1000, target)
                .during(function () {
                    if (obj.alpha != null) {
                        self.setAlpha(obj.alpha);
                    }
                    if (obj.beta != null) {
                        self.setBeta(obj.beta);
                    }
                    if (obj.distance != null) {
                        self.setDistance(obj.distance);
                    }
                    if (obj.center != null) {
                        self.setCenter(obj.center);
                    }
                    if (obj.orthographicSize != null) {
                        self.setOrthographicSize(obj.orthographicSize);
                    }
                    self._needsUpdate = true;
                })
        ).start(opts.easing || 'linear');
    },

    /**
     * Stop all animation
     */
    stopAllAnimation: function () {
        for (var i = 0; i < this._animators.length; i++) {
            this._animators[i].stop();
        }
        this._animators.length = 0;
    },

    update: function () {
        this._needsUpdate = true;
        this._update(20);
    },

    _isAnimating: function () {
        return this._animators.length > 0;
    },
    /**
     * Call update each frame
     * @param  {number} deltaTime Frame time
     */
    _update: function (deltaTime) {

        if (this._rotating) {
            var radian = (this.autoRotateDirection === 'cw' ? 1 : -1)
                 * this.autoRotateSpeed / 180 * Math.PI;
            this._phi -= radian * deltaTime / 1000;
            this._needsUpdate = true;
        }
        else if (this._rotateVelocity.len() > 0) {
            this._needsUpdate = true;
        }

        if (Math.abs(this._zoomSpeed) > 0.1 || this._panVelocity.len() > 0) {
            this._needsUpdate = true;
        }

        if (!this._needsUpdate) {
            return;
        }

        deltaTime = Math.min(deltaTime, 50);

        this._updateDistanceOrSize(deltaTime);

        this._updatePan(deltaTime);

        this._updateRotate(deltaTime);

        this._updateTransform();

        this.getCamera().update();

        this.zr && this.zr.refresh();

        this.trigger('update');

        this._needsUpdate = false;
    },

    _updateRotate: function (deltaTime) {
        var velocity = this._rotateVelocity;
        this._phi = velocity.y * deltaTime / 20 + this._phi;
        this._theta = velocity.x * deltaTime / 20 + this._theta;

        this.setAlpha(this.getAlpha());
        this.setBeta(this.getBeta());

        this._vectorDamping(velocity, Math.pow(this.damping, deltaTime / 16));
    },

    _updateDistanceOrSize: function (deltaTime) {
        if (this._projection === 'perspective') {
            this._setDistance(this._distance + this._zoomSpeed * deltaTime / 20);
        }
        else {
            this._setOrthoSize(this._orthoSize + this._zoomSpeed * deltaTime / 20);
        }

        this._zoomSpeed *= Math.pow(this.damping, deltaTime / 16);
    },


    _setDistance: function (distance) {
        this._distance = Math.max(Math.min(distance, this.maxDistance), this.minDistance);
    },

    _setOrthoSize: function (size) {
        this._orthoSize = Math.max(Math.min(size, this.maxOrthographicSize), this.minOrthographicSize);
        var camera = this.getCamera();
        var cameraHeight = this._orthoSize;
        var cameraWidth = cameraHeight / this.viewGL.viewport.height * this.viewGL.viewport.width;
        camera.left = -cameraWidth / 2;
        camera.right = cameraWidth / 2;
        camera.top = cameraHeight / 2;
        camera.bottom = -cameraHeight / 2;
    },

    _updatePan: function (deltaTime) {

        var velocity = this._panVelocity;
        var len = this._distance;

        var target = this.getCamera();
        var yAxis = target.worldTransform.y;
        var xAxis = target.worldTransform.x;

        // PENDING
        this._center
            .scaleAndAdd(xAxis, -velocity.x * len / 200)
            .scaleAndAdd(yAxis, -velocity.y * len / 200);

        this._vectorDamping(velocity, 0);
    },

    _updateTransform: function () {
        var camera = this.getCamera();

        var dir = new Vector3();
        var theta = this._theta + Math.PI / 2;
        var phi = this._phi + Math.PI / 2;
        var r = Math.sin(theta);

        dir.x = r * Math.cos(phi);
        dir.y = -Math.cos(theta);
        dir.z = r * Math.sin(phi);

        camera.position.copy(this._center).scaleAndAdd(dir, this._distance);
        camera.rotation.identity()
            // First around y, then around x
            .rotateY(-this._phi)
            .rotateX(-this._theta);
    },

    _startCountingStill: function () {
        clearTimeout(this._stillTimeout);

        var time = this.autoRotateAfterStill;
        var self = this;
        if (!isNaN(time) && time > 0) {
            this._stillTimeout = setTimeout(function () {
                self._rotating = true;
            }, time * 1000);
        }
    },

    _vectorDamping: function (v, damping) {
        var speed = v.len();
        speed = speed * damping;
        if (speed < 1e-4) {
            speed = 0;
        }
        v.normalize().scale(speed);
    },

    _decomposeTransform: function () {
        if (!this.getCamera()) {
            return;
        }

        this.getCamera().updateWorldTransform();

        var forward = this.getCamera().worldTransform.z;
        var alpha = Math.asin(forward.y);
        var beta = Math.atan2(forward.x, forward.z);

        this._theta = alpha;
        this._phi = -beta;

        this.setBeta(this.getBeta());
        this.setAlpha(this.getAlpha());

        // Is perspective
        if (this.getCamera().aspect) {
            this._setDistance(this.getCamera().position.dist(this._center));
        }
        else {
            this._setOrthoSize(this.getCamera().top - this.getCamera().bottom);
        }
    },

    _mouseDownHandler: function (e) {
        if (e.target) {
            // If mouseon some zrender element.
            return;
        }
        if (this._isAnimating()) {
            return;
        }

        var x = e.offsetX;
        var y = e.offsetY;
        if (this.viewGL && !this.viewGL.containPoint(x, y)) {
            return;
        }

        this.zr.on('mousemove', this._mouseMoveHandler);
        this.zr.on('mouseup', this._mouseUpHandler);

        if (e.event.targetTouches) {
            if (e.event.targetTouches.length === 1) {
                this._mode = 'rotate';
            }
        }
        else {
            if (e.event.button === MOUSE_BUTTON_KEY_MAP[this.rotateMouseButton]) {
                this._mode = 'rotate';
            }
            else if (e.event.button === MOUSE_BUTTON_KEY_MAP[this.panMouseButton]) {
                this._mode = 'pan';
            }
            else {
                this._mode = '';
            }
        }

        // Reset rotate velocity
        this._rotateVelocity.set(0, 0);
        this._rotating = false;
        if (this.autoRotate) {
            this._startCountingStill();
        }

        this._mouseX = e.offsetX;
        this._mouseY = e.offsetY;
    },

    _mouseMoveHandler: function (e) {
        if (e.target && e.target.__isGLToZRProxy) {
            return;
        }

        if (this._isAnimating()) {
            return;
        }

        var panSensitivity = convertToArray(this.panSensitivity);
        var rotateSensitivity = convertToArray(this.rotateSensitivity);

        if (this._mode === 'rotate') {
            this._rotateVelocity.y = (e.offsetX - this._mouseX) / this.zr.getHeight() * 2 * rotateSensitivity[0];
            this._rotateVelocity.x = (e.offsetY - this._mouseY) / this.zr.getWidth() * 2 * rotateSensitivity[1];
        }
        else if (this._mode === 'pan') {
            this._panVelocity.x = (e.offsetX - this._mouseX) / this.zr.getWidth() * panSensitivity[0] * 400;
            this._panVelocity.y = (-e.offsetY + this._mouseY) / this.zr.getHeight() * panSensitivity[1] * 400;
        }


        this._mouseX = e.offsetX;
        this._mouseY = e.offsetY;

        e.event.preventDefault();
    },

    _mouseWheelHandler: function (e) {
        if (this._isAnimating()) {
            return;
        }
        var delta = e.event.wheelDelta // Webkit
                || -e.event.detail; // Firefox
        this._zoomHandler(e, delta);
    },

    _pinchHandler: function (e) {
        if (this._isAnimating()) {
            return;
        }
        this._zoomHandler(e, e.pinchScale > 1 ? 1 : -1);
        // Not rotate when pinch
        this._mode = '';
    },

    _zoomHandler: function (e, delta) {
        if (delta === 0) {
            return;
        }

        var x = e.offsetX;
        var y = e.offsetY;
        if (this.viewGL && !this.viewGL.containPoint(x, y)) {
            return;
        }

        var speed;
        if (this._projection === 'perspective') {
            speed = Math.max(Math.max(Math.min(
                this._distance - this.minDistance,
                this.maxDistance - this._distance
            )) / 20, 0.5);
        }
        else {
            speed = Math.max(Math.max(Math.min(
                this._orthoSize - this.minOrthographicSize,
                this.maxOrthographicSize - this._orthoSize
            )) / 20, 0.5);
        }
        this._zoomSpeed = (delta > 0 ? -1 : 1) * speed * this.zoomSensitivity;

        this._rotating = false;

        if (this.autoRotate && this._mode === 'rotate') {
            this._startCountingStill();
        }

        e.event.preventDefault();
    },

    _mouseUpHandler: function () {
        this.zr.off('mousemove', this._mouseMoveHandler);
        this.zr.off('mouseup', this._mouseUpHandler);
    },

    _isRightMouseButtonUsed: function () {
        return this.rotateMouseButton === 'right'
            || this.panMouseButton === 'right';
    },

    _contextMenuHandler: function (e) {
        if (this._isRightMouseButtonUsed()) {
            e.preventDefault();
        }
    },

    _addAnimator: function (animator) {
        var animators = this._animators;
        animators.push(animator);
        animator.done(function () {
            var idx = animators.indexOf(animator);
            if (idx >= 0) {
                animators.splice(idx, 1);
            }
        });
        return animator;
    }
});

/**
 * If auto rotate the target
 * @type {boolean}
 * @default false
 */
Object.defineProperty(OrbitControl.prototype, 'autoRotate', {
    get: function (val) {
        return this._autoRotate;
    },
    set: function (val) {
        this._autoRotate = val;
        this._rotating = val;
    }
});


export default OrbitControl;