#include "mac/MacMouse.h"
#include "mac/MacInputManager.h"
#include "mac/MacHelpers.h"
#include "OISException.h"
#include "OISEvents.h"

#include <Carbon/Carbon.h>

#include <list>

#include <iostream>

using namespace OIS;

//Events we subscribe to and remove from queue
const EventTypeSpec mouseEvents[] = {
	{ kEventClassMouse, kEventMouseDown },
	{ kEventClassMouse, kEventMouseUp },
	{ kEventClassMouse, kEventMouseMoved },
	{ kEventClassMouse, kEventMouseDragged },
	{ kEventClassMouse, kEventMouseWheelMoved }		
};

const EventTypeSpec WinFocusAcquired [] = {{kEventClassApplication, kEventAppDeactivated}};

//-------------------------------------------------------------------//
MacMouse::MacMouse( InputManager* creator, bool buffered )
	: Mouse(creator->inputSystemName(), buffered, 0, creator), mNeedsToRegainFocus( false )
{
    mouseEventRef = NULL;
	mWindowFocusHandler = NULL;

    // Get a "Univeral procedure pointer" for our callback
    mouseUPP = NewEventHandlerUPP(MouseWrapper);
	mWindowFocusListener = NewEventHandlerUPP(WindowFocusChanged);

	static_cast<MacInputManager*>(mCreator)->_setMouseUsed(true);
}

MacMouse::~MacMouse()
{
    if(mouseEventRef != NULL)
		RemoveEventHandler(mouseEventRef);

	if(mWindowFocusHandler != NULL)
		RemoveEventHandler(mWindowFocusHandler);
	
	DisposeEventHandlerUPP(mouseUPP);
	DisposeEventHandlerUPP(mWindowFocusListener);
	
	// Restore Mouse
	CGAssociateMouseAndMouseCursorPosition(TRUE);
	CGDisplayShowCursor(kCGDirectMainDisplay);

	static_cast<MacInputManager*>(mCreator)->_setMouseUsed(false);
}

void MacMouse::_initialize()
{
	mState.clear();
	mTempState.clear();
	mMouseWarped = false;
	
	// Hide OS Mouse
 	CGDisplayHideCursor(kCGDirectMainDisplay);

	MacInputManager* im = static_cast<MacInputManager*>(mCreator);
	WindowRef win = im->_getWindow();
	
	if(win)
	{
		Rect clipRect = {0.0f, 0.0f, 0.0f, 0.0f};
		GetWindowBounds(win, kWindowContentRgn, &clipRect);
		
		CGPoint warpPoint;
		warpPoint.x = ((clipRect.right - clipRect.left) / 2) + clipRect.left;
		warpPoint.y = ((clipRect.bottom - clipRect.top) / 2) + clipRect.top;
		CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, warpPoint); //Place at display origin
		
		mMouseWarped = true;
	}

	//Now that mouse is warped, start listening for events
	EventTargetRef event = ((MacInputManager*)mCreator)->_getEventTarget();
    
	if(mouseEventRef != NULL)
		RemoveEventHandler(mouseEventRef);
		
	if(mWindowFocusHandler != NULL)
		RemoveEventHandler(mWindowFocusHandler);

	mouseEventRef = mWindowFocusHandler = NULL;

	if(InstallEventHandler(event, mouseUPP, GetEventTypeCount(mouseEvents), mouseEvents, this, &mouseEventRef) != noErr)
		OIS_EXCEPT( E_General, "MacMouse::_initialize >> Error loading Mouse event handler" );

	if(InstallEventHandler(event, mWindowFocusListener, GetEventTypeCount(WinFocusAcquired), WinFocusAcquired, this, &mWindowFocusHandler) != noErr)
		OIS_EXCEPT( E_General, "MacMouse::_initialize >> Error loading Mouse event handler" );		

	//Lock OS Mouse movement
	mNeedsToRegainFocus = false;
	CGAssociateMouseAndMouseCursorPosition(FALSE);
}

OSStatus MacMouse::WindowFocusChanged(EventHandlerCallRef nextHandler, EventRef event, void* macMouse)
{
	//std::cout << "Window Focus Changed\n";

	MacMouse* _this = static_cast<MacMouse*>(macMouse);
    if (_this)
	{
		_this->mNeedsToRegainFocus = true;
		CGAssociateMouseAndMouseCursorPosition(TRUE);

        // propagate the event down the chain
        return CallNextEventHandler(nextHandler, event);
    }
    else
        OIS_EXCEPT(E_General, "MouseWrapper >> Being called by something other than our event handler!");
}

void MacMouse::setBuffered( bool buffered )
{
	mBuffered = buffered;
}

void MacMouse::capture()
{
	mState.X.rel = 0;
	mState.Y.rel = 0;
	mState.Z.rel = 0;
	    
	if(mTempState.X.rel || mTempState.Y.rel || mTempState.Z.rel)
	{
		//printf("%i %i %i\n\n", mTempState.X.rel, mTempState.Y.rel, mTempState.Z.rel);

		//Set new relative motion values
		mState.X.rel = mTempState.X.rel;
		mState.Y.rel = mTempState.Y.rel;
		mState.Z.rel = mTempState.Z.rel;
		
		//Update absolute position
		mState.X.abs += mTempState.X.rel;
		mState.Y.abs += mTempState.Y.rel;
		
		if(mState.X.abs > mState.width)
			mState.X.abs = mState.width;
		else if(mState.X.abs < 0)
			mState.X.abs = 0;

		if(mState.Y.abs > mState.height)
			mState.Y.abs = mState.height;
		else if(mState.Y.abs < 0)
			mState.Y.abs = 0;
			
		mState.Z.abs += mTempState.Z.rel;
		
		//Fire off event
		if(mListener && mBuffered)
			mListener->mouseMoved(MouseEvent(this, mState));
	}

	mTempState.clear();
}

void MacMouse::_mouseCallback( EventRef theEvent )
{
	OSStatus result = eventNotHandledErr;
    UInt32 kind = GetEventKind (theEvent);

	switch(kind)
	{
		case kEventMouseDragged:
		case kEventMouseMoved:
		{
			//HIPoint location = {0.0f, 0.0f};
			HIPoint delta = {0.0f, 0.0f};
			//Rect clipRect = {0.0f, 0.0f, 0.0f, 0.0f};
			
			if(mNeedsToRegainFocus)
				break;

			// Capture the parameters
			// TODO: Look into HIViewNewTrackingArea
			//GetEventParameter(theEvent, kEventParamMouseLocation, typeHIPoint, NULL, sizeof(HIPoint), NULL, &location);
			GetEventParameter(theEvent, kEventParamMouseDelta, typeHIPoint, NULL, sizeof(HIPoint), NULL, &delta);
			
			// Mouse X and Y are the position on the screen, 
			// startng from top-left at 0,0 caps at full monitor resolution
			
			// If we have a window we need to return adjusted coordinates
			// If not, just use raw coordinates - only do this if showing OS mouse
			//MacInputManager* im = static_cast<MacInputManager*>(mCreator);
			//WindowRef win = im->_getWindow();
			
			//if(win != NULL)
			//{
			//	GetWindowBounds(win, kWindowContentRgn, &clipRect);
			//}
            //else
            //{
            //    clipRect.right = mState.width;
            //    clipRect.bottom = mState.height;
            //}
                
            // clip the mouse, absolute positioning
            //if (location.x <= clipRect.left)
			//	mState.X.abs = 0;
			//else if(location.x >= clipRect.right)
			//	mState.X.abs = clipRect.right - clipRect.left;
			//else
			//	mState.X.abs = location.x - clipRect.left;
			
			//if (location.y <= clipRect.top)
			//	mState.Y.abs = 0;
			//else if(location.y >= clipRect.bottom)
			//	mState.Y.abs = clipRect.bottom - clipRect.top;
			//else
			//	mState.Y.abs = location.y - clipRect.top;
			
			// relative positioning
			if(!mMouseWarped)
			{
				mTempState.X.rel += delta.x;
				mTempState.Y.rel += delta.y;
			}
			
			mMouseWarped = false;

			break;
		}
		case kEventMouseDown:
		{
			EventMouseButton button = 0;
			int mouseButton = 3;
			UInt32 modifiers = 0;
			
			if(mNeedsToRegainFocus)
				break;

			// Capture parameters
			GetEventParameter(theEvent, kEventParamMouseButton, typeMouseButton, NULL, sizeof(EventMouseButton), NULL, &button);
			GetEventParameter(theEvent, kEventParamKeyModifiers, typeUInt32, NULL, sizeof(UInt32), NULL, &modifiers);
			
			if((button == kEventMouseButtonTertiary) || ((button == kEventMouseButtonPrimary) && (modifiers & optionKey)))
			{
				mouseButton = 2;
				mState.buttons |= 1 << mouseButton;
			}
            else if((button == kEventMouseButtonSecondary) || ((button == kEventMouseButtonPrimary) && (modifiers & controlKey)))
            {	
                mouseButton = 1;
                mState.buttons |= 1 << mouseButton;
            }
            else if(button == kEventMouseButtonPrimary)
            {
                mouseButton = 0;
                mState.buttons |= 1 << mouseButton;
            }

            if( mListener && mBuffered )
                mListener->mousePressed( MouseEvent( this, mState ), (MouseButtonID)mouseButton );

            break;
		}
		case kEventMouseUp:
		{
			EventMouseButton button = 0;
			int mouseButton = 3;
			UInt32 modifiers = 0;
			
			if(mNeedsToRegainFocus)
			{
				mNeedsToRegainFocus = false;
				CGAssociateMouseAndMouseCursorPosition(false);
				
				MacInputManager* im = static_cast<MacInputManager*>(mCreator);
				WindowRef win = im->_getWindow();
				
				if(win)
				{
					Rect clipRect = {0.0f, 0.0f, 0.0f, 0.0f};
					GetWindowBounds(win, kWindowContentRgn, &clipRect);
					
					CGPoint warpPoint;
					warpPoint.x = ((clipRect.right - clipRect.left) / 2) + clipRect.left;
					warpPoint.y = ((clipRect.bottom - clipRect.top) / 2) + clipRect.top;
					CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, warpPoint); //Place at display origin
					
					CGDisplayHideCursor(kCGDirectMainDisplay);
					
					mMouseWarped = true;
				}
				
				//Once we regain focus, we do not really know what state all the buttons are in - for now, set to not pressed. todo, check current status
				//compare against old status, and send off any needed events
				mState.buttons = 0;
				
				break;
			}
			
			// Capture parameters
			GetEventParameter(theEvent, kEventParamMouseButton, typeMouseButton, NULL, sizeof(EventMouseButton), NULL, &button);
			GetEventParameter(theEvent, kEventParamKeyModifiers, typeUInt32, NULL, sizeof(UInt32), NULL, &modifiers);
			
			if ((button == kEventMouseButtonTertiary) || ((button == kEventMouseButtonPrimary) && (modifiers & optionKey)))
			{
				mouseButton = 2;
				mState.buttons &= ~(1 << mouseButton);
			}
            else if ((button == kEventMouseButtonSecondary) || ((button == kEventMouseButtonPrimary) && (modifiers & controlKey)))
            {	
                mouseButton = 1;
                mState.buttons &= ~(1 << mouseButton);
            }
            else if (button == kEventMouseButtonPrimary)
            {
                mouseButton = 0;
                mState.buttons &= ~(1 << mouseButton);
            }

            if( mListener && mBuffered )
                mListener->mouseReleased( MouseEvent( this, mState ), (MouseButtonID)mouseButton );

            break;
		}
		case kEventMouseWheelMoved:
		{
			SInt32 wheelDelta = 0;
			EventMouseWheelAxis	wheelAxis = 0; 

			// Capture parameters
			GetEventParameter(theEvent, kEventParamMouseWheelAxis, typeMouseWheelAxis, NULL, sizeof(EventMouseWheelAxis), NULL, &wheelAxis);
			GetEventParameter(theEvent, kEventParamMouseWheelDelta, typeSInt32, NULL, sizeof(SInt32), NULL, &wheelDelta);
			
			// If the Y axis of the wheel changed, then update the Z
			// Does OIS care about the X wheel axis?
			if(wheelAxis == kEventMouseWheelAxisY)
				mTempState.Z.rel += (wheelDelta * 60);

            break;
		}
		default:
			break;
	}    
}
