// This THREEx helper makes it easy to handle the mouse events in your 3D scene
//
// * CHANGES NEEDED
//   * handle drag/drop
//   * notify events not object3D - like DOM
//     * so single object with property
//   * DONE bubling implement bubling/capturing
//   * DONE implement event.stopPropagation()
//   * DONE implement event.type = "click" and co
//   * DONE implement event.target
//
// # Lets get started
//
// First you include it in your page
//
// ```<script src='threex.domevent.js'>< /script>```
//
// # use the object oriented api
//
// You bind an event like this
//
// ```mesh.on('click', function(object3d){ ... })```
//
// To unbind an event, just do
//
// ```mesh.off('click', function(object3d){ ... })```
//
// As an alternative, there is another naming closer DOM events.
// Pick the one you like, they are doing the same thing
//
// ```mesh.addEventListener('click', function(object3d){ ... })```
// ```mesh.removeEventListener('click', function(object3d){ ... })```
//
// # Supported Events
//
// Always in a effort to stay close to usual pratices, the events name are the same as in DOM.
// The semantic is the same too.
// Currently, the available events are
// [click, dblclick, mouseup, mousedown](http://www.quirksmode.org/dom/events/click.html),
// [mouseover and mouse out](http://www.quirksmode.org/dom/events/mouseover.html).
//
// # use the standalone api
//
// The object-oriented api modifies THREE.Object3D class.
// It is a global class, so it may be legitimatly considered unclean by some people.
// If this bother you, simply do ```THREEx.DomEvents.noConflict()``` and use the
// standalone API. In fact, the object oriented API is just a thin wrapper
// on top of the standalone API.
//
// First, you instanciate the object
//
// ```var domEvent = new THREEx.DomEvent();```
//
// Then you bind an event like this
//
// ```domEvent.bind(mesh, 'click', function(object3d){ object3d.scale.x *= 2; });```
//
// To unbind an event, just do
//
// ```domEvent.unbind(mesh, 'click', callback);```
//
//
// # Code

//

/** @namespace */
var THREEx		= THREEx 		|| {};

// # Constructor
THREEx.DomEvents	= function(camera, domElement)
{
    this._camera	= camera || null;
    this._domElement= domElement || document;
    this._raycaster = new THREE.Raycaster();
    this._selected	= null;
    this._boundObjs	= {};
    // Bind dom event for mouse and touch
    var _this	= this;

    this._$onClick		= function(){ _this._onClick.apply(_this, arguments);		};
    this._$onDblClick	= function(){ _this._onDblClick.apply(_this, arguments);	};
    this._$onMouseMove	= function(){ _this._onMouseMove.apply(_this, arguments);	};
    this._$onMouseDown	= function(){ _this._onMouseDown.apply(_this, arguments);	};
    this._$onMouseUp	= function(){ _this._onMouseUp.apply(_this, arguments);		};
    this._$onTouchMove	= function(){ _this._onTouchMove.apply(_this, arguments);	};
    this._$onTouchStart	= function(){ _this._onTouchStart.apply(_this, arguments);	};
    this._$onTouchEnd	= function(){ _this._onTouchEnd.apply(_this, arguments);	};
    this._$onContextmenu	= function(){ _this._onContextmenu.apply(_this, arguments);	};
    this._domElement.addEventListener( 'click'	, this._$onClick	, false );
    this._domElement.addEventListener( 'dblclick'	, this._$onDblClick	, false );
    this._domElement.addEventListener( 'mousemove'	, this._$onMouseMove	, false );
    this._domElement.addEventListener( 'mousedown'	, this._$onMouseDown	, false );
    this._domElement.addEventListener( 'mouseup'	, this._$onMouseUp	, false );
    this._domElement.addEventListener( 'touchmove'	, this._$onTouchMove	, false );
    this._domElement.addEventListener( 'touchstart'	, this._$onTouchStart	, false );
    this._domElement.addEventListener( 'touchend'	, this._$onTouchEnd	, false );
    this._domElement.addEventListener( 'contextmenu', this._$onContextmenu	, false );

}

// # Destructor
THREEx.DomEvents.prototype.destroy	= function()
{
    // unBind dom event for mouse and touch
    this._domElement.removeEventListener( 'click'		, this._$onClick	, false );
    this._domElement.removeEventListener( 'dblclick'	, this._$onDblClick	, false );
    this._domElement.removeEventListener( 'mousemove'	, this._$onMouseMove	, false );
    this._domElement.removeEventListener( 'mousedown'	, this._$onMouseDown	, false );
    this._domElement.removeEventListener( 'mouseup'		, this._$onMouseUp	, false );
    this._domElement.removeEventListener( 'touchmove'	, this._$onTouchMove	, false );
    this._domElement.removeEventListener( 'touchstart'	, this._$onTouchStart	, false );
    this._domElement.removeEventListener( 'touchend'	, this._$onTouchEnd	, false );
    this._domElement.removeEventListener( 'contextmenu'	, this._$onContextmenu	, false );
}

THREEx.DomEvents.eventNames	= [
    "click",
    "dblclick",
    "mouseover",
    "mouseout",
    "mousemove",
    "mousedown",
    "mouseup",
    "contextmenu",
    "touchstart",
    "touchend"
];

THREEx.DomEvents.prototype._getRelativeMouseXY	= function(domEvent){
    var element = domEvent.target || domEvent.srcElement;
    if (element.nodeType === 3) {
        element = element.parentNode; // Safari fix -- see http://www.quirksmode.org/js/events_properties.html
    }

    //get the real position of an element relative to the page starting point (0, 0)
    //credits go to brainjam on answering http://stackoverflow.com/questions/5755312/getting-mouse-position-relative-to-content-area-of-an-element
    var elPosition	= { x : 0 , y : 0};
    var tmpElement	= element;
    //store padding
    var style	= getComputedStyle(tmpElement, null);
    elPosition.y += parseInt(style.getPropertyValue("padding-top"), 10);
    elPosition.x += parseInt(style.getPropertyValue("padding-left"), 10);
    //add positions
    do {
        elPosition.x	+= tmpElement.offsetLeft;
        elPosition.y	+= tmpElement.offsetTop;
        style		= getComputedStyle(tmpElement, null);

        elPosition.x	+= parseInt(style.getPropertyValue("border-left-width"), 10);
        elPosition.y	+= parseInt(style.getPropertyValue("border-top-width"), 10);
    } while(tmpElement = tmpElement.offsetParent);

    var elDimension	= {
        width	: (element === window) ? window.innerWidth	: element.offsetWidth,
        height	: (element === window) ? window.innerHeight	: element.offsetHeight
    };

    return {
        x : +((domEvent.pageX - elPosition.x) / elDimension.width ) * 2 - 1,
        y : -((domEvent.pageY - elPosition.y) / elDimension.height) * 2 + 1
    };
};


/********************************************************************************/
/*		domevent context						*/
/********************************************************************************/

// handle domevent context in object3d instance

THREEx.DomEvents.prototype._objectCtxInit	= function(object3d){
    object3d._3xDomEvent = {};
}
THREEx.DomEvents.prototype._objectCtxDeinit	= function(object3d){
    delete object3d._3xDomEvent;
}
THREEx.DomEvents.prototype._objectCtxIsInit	= function(object3d){
    return object3d._3xDomEvent ? true : false;
}
THREEx.DomEvents.prototype._objectCtxGet		= function(object3d){
    return object3d._3xDomEvent;
}

/********************************************************************************/
/*										*/
/********************************************************************************/

/**
 * Getter/Setter for camera
 */
THREEx.DomEvents.prototype.camera	= function(value)
{
    if( value )	this._camera	= value;
    return this._camera;
}

THREEx.DomEvents.prototype.bind	= function(object3d, eventName, callback, useCapture)
{
    console.assert( THREEx.DomEvents.eventNames.indexOf(eventName) !== -1, "not available events:"+eventName );

    if( !this._objectCtxIsInit(object3d) )	this._objectCtxInit(object3d);
    var objectCtx	= this._objectCtxGet(object3d);
    if( !objectCtx[eventName+'Handlers'] )	objectCtx[eventName+'Handlers']	= [];

    objectCtx[eventName+'Handlers'].push({
        callback	: callback,
        useCapture	: useCapture
    });

    // add this object in this._boundObjs
    if( this._boundObjs[eventName] === undefined ){
        this._boundObjs[eventName]	= [];
    }
    this._boundObjs[eventName].push(object3d);
}
THREEx.DomEvents.prototype.addEventListener	= THREEx.DomEvents.prototype.bind

THREEx.DomEvents.prototype.unbind	= function(object3d, eventName, callback, useCapture)
{
    console.assert( THREEx.DomEvents.eventNames.indexOf(eventName) !== -1, "not available events:"+eventName );

    if( !this._objectCtxIsInit(object3d) )	this._objectCtxInit(object3d);

    var objectCtx	= this._objectCtxGet(object3d);
    if( !objectCtx[eventName+'Handlers'] )	objectCtx[eventName+'Handlers']	= [];

    var handlers	= objectCtx[eventName+'Handlers'];
    for(var i = 0; i < handlers.length; i++){
        var handler	= handlers[i];
        if( callback != handler.callback )	continue;
        if( useCapture != handler.useCapture )	continue;
        handlers.splice(i, 1)
        break;
    }
    // from this object from this._boundObjs
    var index	= this._boundObjs[eventName].indexOf(object3d);
    console.assert( index !== -1 );
    this._boundObjs[eventName].splice(index, 1);
}
THREEx.DomEvents.prototype.removeEventListener	= THREEx.DomEvents.prototype.unbind

THREEx.DomEvents.prototype._bound	= function(eventName, object3d)
{
    var objectCtx	= this._objectCtxGet(object3d);
    if( !objectCtx )	return false;
    return objectCtx[eventName+'Handlers'] ? true : false;
}

/********************************************************************************/
/*		onMove								*/
/********************************************************************************/

// # handle mousemove kind of events

THREEx.DomEvents.prototype._onMove	= function(eventName, mouseX, mouseY, origDomEvent)
{
//console.log('eventName', eventName, 'boundObjs', this._boundObjs[eventName])
    // get objects bound to this event
    var boundObjs	= this._boundObjs[eventName];
    if( boundObjs === undefined || boundObjs.length === 0 )	return;
    // compute the intersection
    var vector = new THREE.Vector2();

    // update the picking ray with the camera and mouse position
    vector.set( mouseX, mouseY );
    this._raycaster.setFromCamera( vector, this._camera );

    var intersects = this._raycaster.intersectObjects( boundObjs );

    var oldSelected	= this._selected;

    if( intersects.length > 0 ){
        var notifyOver, notifyOut, notifyMove;
        var intersect	= intersects[ 0 ];
        var newSelected	= intersect.object;
        this._selected	= newSelected;
        // if newSelected bound mousemove, notify it
        notifyMove	= this._bound('mousemove', newSelected);

        if( oldSelected != newSelected ){
            // if newSelected bound mouseenter, notify it
            notifyOver	= this._bound('mouseover', newSelected);
            // if there is a oldSelect and oldSelected bound mouseleave, notify it
            notifyOut	= oldSelected && this._bound('mouseout', oldSelected);
        }
    }else{
        // if there is a oldSelect and oldSelected bound mouseleave, notify it
        notifyOut	= oldSelected && this._bound('mouseout', oldSelected);
        this._selected	= null;
    }


    // notify mouseMove - done at the end with a copy of the list to allow callback to remove handlers
    notifyMove && this._notify('mousemove', newSelected, origDomEvent, intersect);
    // notify mouseEnter - done at the end with a copy of the list to allow callback to remove handlers
    notifyOver && this._notify('mouseover', newSelected, origDomEvent, intersect);
    // notify mouseLeave - done at the end with a copy of the list to allow callback to remove handlers
    notifyOut  && this._notify('mouseout' , oldSelected, origDomEvent, intersect);
}


/********************************************************************************/
/*		onEvent								*/
/********************************************************************************/

// # handle click kind of events

THREEx.DomEvents.prototype._onEvent	= function(eventName, mouseX, mouseY, origDomEvent)
{
    //console.log('eventName', eventName, 'boundObjs', this._boundObjs[eventName])
    // get objects bound to this event
    var boundObjs	= this._boundObjs[eventName];
    if( boundObjs === undefined || boundObjs.length === 0 )	return;
    // compute the intersection
    var vector = new THREE.Vector2();

    // update the picking ray with the camera and mouse position
    vector.set( mouseX, mouseY );
    this._raycaster.setFromCamera( vector, this._camera );

    var intersects = this._raycaster.intersectObjects( boundObjs, true);
    // if there are no intersections, return now
    if( intersects.length === 0 )	return;

    // init some variables
    var intersect	= intersects[0];
    var object3d	= intersect.object;
    var objectCtx	= this._objectCtxGet(object3d);
    var objectParent = object3d.parent;

    while ( typeof(objectCtx) == 'undefined' && objectParent )
    {
        objectCtx = this._objectCtxGet(objectParent);
        objectParent = objectParent.parent;
    }
    if( !objectCtx )	return;

    // notify handlers
    this._notify(eventName, object3d, origDomEvent, intersect);
}

THREEx.DomEvents.prototype._notify	= function(eventName, object3d, origDomEvent, intersect)
{
    var objectCtx	= this._objectCtxGet(object3d);
    var handlers	= objectCtx ? objectCtx[eventName+'Handlers'] : null;

    // parameter check
    console.assert(arguments.length === 4)

    // do bubbling
    if( !objectCtx || !handlers || handlers.length === 0 ){
        object3d.parent && this._notify(eventName, object3d.parent, origDomEvent, intersect);
        return;
    }

    // notify all handlers
    var handlers	= objectCtx[eventName+'Handlers'];
    for(var i = 0; i < handlers.length; i++){
        var handler	= handlers[i];
        var toPropagate	= true;
        handler.callback({
            type		: eventName,
            target		: object3d,
            origDomEvent	: origDomEvent,
            intersect	: intersect,
            stopPropagation	: function(){
                toPropagate	= false;
            }
        });
        if( !toPropagate )	continue;
        // do bubbling
        if( handler.useCapture === false ){
            object3d.parent && this._notify(eventName, object3d.parent, origDomEvent, intersect);
        }
    }
}

/********************************************************************************/
/*		handle mouse events						*/
/********************************************************************************/
// # handle mouse events

THREEx.DomEvents.prototype._onMouseDown	= function(event){ return this._onMouseEvent('mousedown', event);	}
THREEx.DomEvents.prototype._onMouseUp	= function(event){ return this._onMouseEvent('mouseup'	, event);	}


THREEx.DomEvents.prototype._onMouseEvent	= function(eventName, domEvent)
{
    var mouseCoords = this._getRelativeMouseXY(domEvent);
    this._onEvent(eventName, mouseCoords.x, mouseCoords.y, domEvent);
}

THREEx.DomEvents.prototype._onMouseMove	= function(domEvent)
{
    var mouseCoords = this._getRelativeMouseXY(domEvent);
    this._onMove('mousemove', mouseCoords.x, mouseCoords.y, domEvent);
    this._onMove('mouseover', mouseCoords.x, mouseCoords.y, domEvent);
    this._onMove('mouseout' , mouseCoords.x, mouseCoords.y, domEvent);
}

THREEx.DomEvents.prototype._onClick		= function(event)
{
    // TODO handle touch ?
    this._onMouseEvent('click'	, event);
}
THREEx.DomEvents.prototype._onDblClick		= function(event)
{
    // TODO handle touch ?
    this._onMouseEvent('dblclick'	, event);
}

THREEx.DomEvents.prototype._onContextmenu	= function(event)
{
    //TODO don't have a clue about how this should work with touch..
    this._onMouseEvent('contextmenu'	, event);
}

/********************************************************************************/
/*		handle touch events						*/
/********************************************************************************/
// # handle touch events


THREEx.DomEvents.prototype._onTouchStart	= function(event){ return this._onTouchEvent('touchstart', event);	}
THREEx.DomEvents.prototype._onTouchEnd	= function(event){ return this._onTouchEvent('touchend'	, event);	}

THREEx.DomEvents.prototype._onTouchMove	= function(domEvent)
{
    if( domEvent.touches.length != 1 )	return undefined;

    domEvent.preventDefault();

    var mouseX	= +(domEvent.touches[ 0 ].pageX / window.innerWidth ) * 2 - 1;
    var mouseY	= -(domEvent.touches[ 0 ].pageY / window.innerHeight) * 2 + 1;
    this._onMove('mousemove', mouseX, mouseY, domEvent);
    this._onMove('mouseover', mouseX, mouseY, domEvent);
    this._onMove('mouseout' , mouseX, mouseY, domEvent);
}

/*THREEx.DomEvents.prototype._onTouchEvent	= function(eventName, domEvent)
{
    if( domEvent.touches.length != 1 )	return undefined;

    domEvent.preventDefault();

    var mouseX	= +(domEvent.touches[ 0 ].pageX / window.innerWidth ) * 2 - 1;
    var mouseY	= -(domEvent.touches[ 0 ].pageY / window.innerHeight) * 2 + 1;
    this._onEvent(eventName, mouseX, mouseY, domEvent);
}*/

THREEx.DomEvents.prototype._onTouchEvent	= function(eventName, domEvent)
{
    if (domEvent.touches.length != 1 && eventName != "touchend")
        return undefined;

    domEvent.preventDefault();
    var touches = eventName != "touchend" ? domEvent.touches : domEvent.changedTouches;
    var mouseX = +(touches[0].pageX / window.innerWidth) * 2 - 1;
    var mouseY = -(touches[0].pageY / window.innerHeight) * 2 + 1;
    this._onEvent(eventName, mouseX, mouseY, domEvent);
}