import {
    EventDispatcher,
    Matrix4,
    Plane,
    Raycaster,
    Vector2,
    Vector3
} from 'three';

const _plane = new Plane();
const _raycaster = new Raycaster();

const _pointer = new Vector2();
const _offset = new Vector3();
const _intersection = new Vector3();
const _worldPosition = new Vector3();
const _inverseMatrix = new Matrix4();

class DragControls extends EventDispatcher {

    constructor(_objects, _camera, _domElement) {

        super();

        _domElement.style.touchAction = 'none'; // disable touch scroll

        let _selected = null, _hovered = null;

        const _intersections = [];

        //

        const scope = this;

        function activate() {

            _domElement.addEventListener('pointermove', onPointerMove);
            _domElement.addEventListener('pointerdown', onPointerDown);
            _domElement.addEventListener('pointerup', onPointerCancel);
            _domElement.addEventListener('pointerleave', onPointerCancel);

        }

        function deactivate() {

            _domElement.removeEventListener('pointermove', onPointerMove);
            _domElement.removeEventListener('pointerdown', onPointerDown);
            _domElement.removeEventListener('pointerup', onPointerCancel);
            _domElement.removeEventListener('pointerleave', onPointerCancel);

            _domElement.style.cursor = '';

        }

        function dispose() {

            deactivate();

        }

        function getObjects() {

            return _objects;

        }

        function getRaycaster() {

            return _raycaster;

        }

        function onPointerMove(event) {

            if (scope.enabled === false) return;

            updatePointer(event);

            _raycaster.setFromCamera(_pointer, _camera);

            if (_selected) {

                if (_raycaster.ray.intersectPlane(_plane, _intersection)) {

                    _selected.position.copy(_intersection.sub(_offset).applyMatrix4(_inverseMatrix));

                }

                scope.dispatchEvent({ type: 'drag', object: _selected });

                return;

            }

            // hover support

            if (event.pointerType === 'mouse' || event.pointerType === 'pen') {

                _intersections.length = 0;

                _raycaster.setFromCamera(_pointer, _camera);
                _raycaster.intersectObjects(_objects, true, _intersections);

                if (_intersections.length > 0) {

                    const object = _intersections[0].object;

                    _plane.setFromNormalAndCoplanarPoint(_camera.getWorldDirection(_plane.normal), _worldPosition.setFromMatrixPosition(object.matrixWorld));

                    if (_hovered !== object && _hovered !== null) {

                        scope.dispatchEvent({ type: 'hoveroff', object: _hovered });

                        _domElement.style.cursor = 'auto';
                        _hovered = null;

                    }

                    if (_hovered !== object) {

                        scope.dispatchEvent({ type: 'hoveron', object: object });

                        _domElement.style.cursor = 'pointer';
                        _hovered = object;

                    }

                } else {

                    if (_hovered !== null) {

                        scope.dispatchEvent({ type: 'hoveroff', object: _hovered });

                        _domElement.style.cursor = 'auto';
                        _hovered = null;

                    }

                }

            }

        }

        function onPointerDown(event) {

            if (scope.enabled === false) return;

            updatePointer(event);

            _intersections.length = 0;

            _raycaster.setFromCamera(_pointer, _camera);
            _raycaster.intersectObjects(_objects, true, _intersections);

            if (_intersections.length > 0) {

                _selected = (scope.transformGroup === true) ? _objects[0] : _intersections[0].object;

                _plane.setFromNormalAndCoplanarPoint(_camera.getWorldDirection(_plane.normal), _worldPosition.setFromMatrixPosition(_selected.matrixWorld));

                if (_raycaster.ray.intersectPlane(_plane, _intersection)) {

                    _inverseMatrix.copy(_selected.parent.matrixWorld).invert();
                    _offset.copy(_intersection).sub(_worldPosition.setFromMatrixPosition(_selected.matrixWorld));
                }

                _domElement.style.cursor = 'move';

                scope.dispatchEvent({ type: 'dragstart', object: _selected });

            }


        }

        function onPointerCancel() {

            if (scope.enabled === false) return;

            if (_selected) {

                scope.dispatchEvent({ type: 'dragend', object: _selected });

                _selected = null;

            }

            _domElement.style.cursor = _hovered ? 'pointer' : 'auto';

        }

        function updatePointer(event) {

            const rect = _domElement.getBoundingClientRect();

            _pointer.x = (event.clientX - rect.left) / rect.width * 2 - 1;
            _pointer.y = - (event.clientY - rect.top) / rect.height * 2 + 1;

        }

        function setSelected(newSelected){
            _selected = newSelected;
            if(_selected){
                _plane.setFromNormalAndCoplanarPoint(_camera.getWorldDirection(_plane.normal), _worldPosition.setFromMatrixPosition(_selected.matrixWorld));
                if (_raycaster.ray.intersectPlane(_plane, _intersection)) {
    
                    _inverseMatrix.copy(_selected.parent.matrixWorld).invert();
                    _offset.copy(_intersection).sub(_worldPosition.setFromMatrixPosition(_selected.matrixWorld));
                }
            }
        }

        activate();

        // API

        this.enabled = true;
        this.transformGroup = false;

        this.activate = activate;
        this.deactivate = deactivate;
        this.dispose = dispose;
        this.getObjects = getObjects;
        this.getRaycaster = getRaycaster;
        this.setSelected = setSelected;

    }

}

export { DragControls };