/*
    This file is part of Mitsuba, a physically based rendering system.

    Copyright (c) 2007-2014 by Wenzel Jakob and others.

    Mitsuba is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License Version 3
    as published by the Free Software Foundation.

    Mitsuba is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include <mitsuba/hw/glrenderer.h>
#include <mitsuba/hw/nsgldevice.h>
#include <mitsuba/hw/nsglkeys.h>
#include <mitsuba/hw/nsglrenderer.h>

using namespace mitsuba;

#define DEFINE_KEY(xsym, sym) m_keymap[xsym] = Device::sym

@implementation CustomView
- (id) initWithFrame: (NSRect) frame {
    self = [super initWithFrame: frame];
    if (self) {
        m_modifiers = 0;
        m_firstMouseMotion = true;
        m_focused = false;
        m_mouseInWindow = false;
        m_ignoreNextMouseEvent = false;

        /* Cocoa <-> Mitsuba keyboard mappings */
        for (uint i=0; i<0xFF; i++)
            m_keymap[i] = Device::ENoSpecial;
        DEFINE_KEY(QZ_BACKSPACE, EKeyBackspace);
        DEFINE_KEY(QZ_TAB, EKeyTab);
        DEFINE_KEY(QZ_RETURN, EKeyReturn);
        DEFINE_KEY(QZ_PAUSE, EKeyPause);
        DEFINE_KEY(QZ_ESCAPE, EKeyEscape);
        DEFINE_KEY(QZ_DELETE, EKeyDelete);
        DEFINE_KEY(QZ_KP0, EKeyKeyPad0);
        DEFINE_KEY(QZ_KP1, EKeyKeyPad1);
        DEFINE_KEY(QZ_KP2, EKeyKeyPad2);
        DEFINE_KEY(QZ_KP3, EKeyKeyPad3);
        DEFINE_KEY(QZ_KP4, EKeyKeyPad4);
        DEFINE_KEY(QZ_KP5, EKeyKeyPad5);
        DEFINE_KEY(QZ_KP6, EKeyKeyPad6);
        DEFINE_KEY(QZ_KP7, EKeyKeyPad7);
        DEFINE_KEY(QZ_KP8, EKeyKeyPad8);
        DEFINE_KEY(QZ_KP9, EKeyKeyPad9);
        DEFINE_KEY(QZ_KP_PERIOD, EKeyKeyPadPeriod);
        DEFINE_KEY(QZ_KP_DIVIDE, EKeyKeyPadDivide);
        DEFINE_KEY(QZ_KP_MULTIPLY, EKeyKeyPadMultiply);
        DEFINE_KEY(QZ_KP_MINUS, EKeyKeyPadMinus);
        DEFINE_KEY(QZ_KP_PLUS, EKeyKeyPadPlus);
        DEFINE_KEY(QZ_KP_ENTER, EKeyKeyPadEnter);
        DEFINE_KEY(QZ_KP_EQUALS, EKeyKeyPadEquals);
        DEFINE_KEY(QZ_UP, EKeyUp);
        DEFINE_KEY(QZ_DOWN, EKeyDown);
        DEFINE_KEY(QZ_RIGHT, EKeyRight);
        DEFINE_KEY(QZ_LEFT, EKeyLeft);
        DEFINE_KEY(QZ_INSERT, EKeyInsert);
        DEFINE_KEY(QZ_HOME, EKeyHome);
        DEFINE_KEY(QZ_END, EKeyEnd);
        DEFINE_KEY(QZ_PAGEUP, EKeyPageUp);
        DEFINE_KEY(QZ_PAGEDOWN, EKeyPageDown);
        DEFINE_KEY(QZ_F1, EKeyF1);
        DEFINE_KEY(QZ_F2, EKeyF2);
        DEFINE_KEY(QZ_F3, EKeyF3);
        DEFINE_KEY(QZ_F4, EKeyF4);
        DEFINE_KEY(QZ_F5, EKeyF5);
        DEFINE_KEY(QZ_F6, EKeyF6);
        DEFINE_KEY(QZ_F7, EKeyF7);
        DEFINE_KEY(QZ_F8, EKeyF8);
        DEFINE_KEY(QZ_F9, EKeyF9);
        DEFINE_KEY(QZ_F10, EKeyF10);
        DEFINE_KEY(QZ_F11, EKeyF11);
        DEFINE_KEY(QZ_F12, EKeyF12);
        DEFINE_KEY(QZ_NUMLOCK, EKeyNumLock);
        DEFINE_KEY(QZ_CAPSLOCK, EKeyCapsLock);
        DEFINE_KEY(QZ_SCROLLOCK, EKeyScrollLock);
        DEFINE_KEY(QZ_LSHIFT, EKeyLShift);
        DEFINE_KEY(QZ_RSHIFT, EKeyRShift);
        DEFINE_KEY(QZ_LMETA, EKeyLMeta);
        DEFINE_KEY(QZ_RMETA, EKeyRMeta);
        DEFINE_KEY(QZ_LALT, EKeyLAlt);
        DEFINE_KEY(QZ_RALT, EKeyRAlt);
        DEFINE_KEY(QZ_LCTRL, EKeyLControl);
        DEFINE_KEY(QZ_RCTRL, EKeyRControl);
    }
    return self;
}

- (void) setDevice: (NSGLDevice *) device {
    m_device = device;
}

- (void) ignoreNextMouseEvent {
    m_ignoreNextMouseEvent = true;
}

- (void) ignoreFirstMouseMotion {
    m_firstMouseMotion = false;
}

- (BOOL) focused {
    return m_focused;
}

- (BOOL) acceptsFirstResponder {
    return YES;
}

- (uint) extractModifiers: (uint) modifiers {
    uint result = 0;

    if (modifiers & NSAlphaShiftKeyMask)
        result |= 0x10;
    if (modifiers & NSShiftKeyMask)
        result |= Device::EShiftModifier;
    if (modifiers & NSControlKeyMask)
        result |= Device::EControlModifier;
    if (modifiers & NSAlternateKeyMask)
        result |= Device::EAltModifier;
    if (modifiers & NSCommandKeyMask)
        result |= Device::EMetaModifier;
    return result;
}

- (void) windowDidBecomeKey: (NSNotification *) notification {
    bool cursorInWindow = m_device->isMouseInWindow();
    m_focused = true;
    m_device->pushEvent(DeviceEvent(Device::EGainFocusEvent));
    m_buttonMask = 0;
    if (!m_device->getCursor() && cursorInWindow) {
        m_mouseInWindow = true;
        [NSCursor hide];
    }
}

- (void) windowDidResignKey: (NSNotification *) notification {
    m_focused = false;
    m_device->pushEvent(DeviceEvent(Device::ELoseFocusEvent));
    m_buttonMask = 0;
    if (!m_device->getCursor()) {
        [NSCursor unhide];
    }
}

- (void) handleEvent: (NSEvent *) event {
    DeviceEvent deviceEvent(Device::ENoEvent);
    uint type = [event type];

    switch (type) {
        case NSFlagsChanged: {
                const uint list1[] = { Device::EShiftModifier, Device::EControlModifier, Device::EAltModifier, Device::EMetaModifier, 0x10};
                const uint list2[] = { Device::EKeyLShift, Device::EKeyLControl, Device::EKeyLAlt, Device::EKeyLMeta, Device::EKeyCapsLock};
                uint newModifiers = [self extractModifiers: [event modifierFlags]];
                uint difference = m_modifiers ^ newModifiers;
                m_modifiers = newModifiers;

                for (uint i=0; i<5; i++) {
                    if ((difference & list1[i]) != 0) {
                        deviceEvent.setType((m_modifiers & list1[i]) == 0 ? Device::EKeyUpEvent : Device::EKeyDownEvent);
                        deviceEvent.getKeyboardInterpreted()[0] = '\0';
                        deviceEvent.setKeyboardSpecial(list2[i]);
                        deviceEvent.setKeyboardKey('\0');
                        deviceEvent.setKeyboardModifiers(m_modifiers);
                        m_device->pushEvent(deviceEvent);
                    }
                }
            }
            return;
        case NSLeftMouseUp:
        case NSRightMouseUp:
        case NSOtherMouseUp:
            deviceEvent.setType(Device::EMouseButtonUpEvent);
        case NSLeftMouseDown:
        case NSRightMouseDown:
        case NSOtherMouseDown: {
                NSPoint location = [((NSWindow *) m_device->getWindow()) mouseLocationOutsideOfEventStream];
                if (deviceEvent.getType() == Device::ENoEvent)
                    deviceEvent.setType(Device::EMouseButtonDownEvent);
                deviceEvent.setMousePosition(Point2i((int) location.x, m_device->getSize().y - (int) location.y));
                deviceEvent.setMouseRelative(Vector2i(0, 0));
                uint buttonNumber = [event buttonNumber];
                uint buttonMask = 0;
                if (buttonNumber == 0)
                    buttonMask = Device::ELeftButton;
                else if (buttonNumber == 1)
                    buttonMask = Device::ERightButton;
                else if (buttonNumber == 2)
                    buttonMask = Device::EMiddleButton;
                else
                    return;
                if (deviceEvent.getType() == Device::EMouseButtonDownEvent)
                    m_buttonMask |= buttonMask;
                else
                    m_buttonMask &= ~buttonMask;
                deviceEvent.setMouseButton(buttonMask);
            }
            break;
        case NSScrollWheel: {
                NSPoint location = [((NSWindow *) m_device->getWindow()) mouseLocationOutsideOfEventStream];
                float deltaX = [event deltaX], deltaY = [event deltaY];
                if (deltaX > 0 || deltaY > 0)
                    deviceEvent.setMouseButton(Device::EWheelUpButton);
                else
                    deviceEvent.setMouseButton(Device::EWheelDownButton);
                deviceEvent.setMousePosition(Point2i((int) location.x, m_device->getSize().y - (int) location.y));
                deviceEvent.setMouseRelative(Vector2i(0, 0));
                deviceEvent.setType(Device::EMouseButtonDownEvent);
                m_device->pushEvent(deviceEvent);
                deviceEvent.setType(Device::EMouseButtonUpEvent);
                m_device->pushEvent(deviceEvent);
                return;
            }
            break;
        case NSLeftMouseDragged:
        case NSRightMouseDragged:
        case NSOtherMouseDragged:
        case NSMouseMoved: {
                if (m_ignoreNextMouseEvent) {
                    m_ignoreNextMouseEvent = false;
                    return;
                }
                NSPoint location = [((NSWindow *) m_device->getWindow()) mouseLocationOutsideOfEventStream];
                Point2i absolute((int) location.x, m_device->getSize().y - (int) location.y);

                bool cursorInWindow = m_device->isMouseInWindow();
                if (m_buttonMask == 0)
                    deviceEvent.setType(Device::EMouseMotionEvent);
                else
                    deviceEvent.setType(Device::EMouseDragEvent);
                deviceEvent.setMousePosition(absolute);
                deviceEvent.setMouseButton(m_buttonMask);
                if (m_firstMouseMotion) {
                    deviceEvent.setMouseRelative(Vector2i(absolute.x, absolute.y));
                    m_firstMouseMotion = false;
                } else {
                    deviceEvent.setMouseRelative(Vector2i((int) [event deltaX], (int) [event deltaY]));
                }

                if (!cursorInWindow && m_focused && !m_device->getCursor()) {
                    if (m_mouseInWindow) {
                        m_mouseInWindow = false;
                        [NSCursor unhide];
                    }
                } else if (cursorInWindow && m_focused && !m_device->getCursor()) {
                    if (!m_mouseInWindow) {
                        m_mouseInWindow = true;
                        [NSCursor hide];
                    }
                }

                if (cursorInWindow)
                    m_mouseInWindow = true;

                if (absolute.x > m_device->getSize().x || absolute.x < 0
                    || absolute.y > m_device->getSize().y || absolute.y< 0)
                    return;
            }
            break;
        case NSKeyUp:
            deviceEvent.setType(Device::EKeyUpEvent);
        case NSKeyDown: {
            if (deviceEvent.getType() == Device::ENoEvent)
                deviceEvent.setType(Device::EKeyDownEvent);
            if ([event isARepeat])
                return;

            NSString *characters = [event characters];
            uint count = [characters length];

            if (count == 0 || count == 1) {
                unsigned char scanCode = [event keyCode];
                uint special = m_keymap[scanCode];

                strncpy(deviceEvent.getKeyboardInterpreted(), [characters UTF8String], 15);

                if (special != 0) {
                    deviceEvent.setKeyboardKey('\0');
                    deviceEvent.setKeyboardSpecial(m_keymap[scanCode]);
                    deviceEvent.setKeyboardModifiers([self extractModifiers: [event modifierFlags]]);
                } else {
                    if (count > 0)
                        deviceEvent.setKeyboardKey([[event charactersIgnoringModifiers] UTF8String][0]);
                    else
                        deviceEvent.setKeyboardKey('\0');
                    deviceEvent.setKeyboardSpecial(Device::ENoSpecial);
                    deviceEvent.setKeyboardModifiers(0);
                }
            }
        }
    }

    if (deviceEvent.getType() != Device::ENoEvent)
        m_device->pushEvent(deviceEvent);
}

- (void) mouseDown: (NSEvent *) event {
    [self handleEvent: event];
}

- (void) mouseUp: (NSEvent *) event {
    [self handleEvent: event];
}

- (void) mouseMoved: (NSEvent *) event {
    [self handleEvent: event];
}

- (void) mouseDragged: (NSEvent *) event {
    [self handleEvent: event];
}

- (void) rightMouseDown: (NSEvent *) event {
    [self handleEvent: event];
}

- (void) rightMouseUp: (NSEvent *) event {
    [self handleEvent: event];
}

- (void) rightMouseMoved: (NSEvent *) event {
    [self handleEvent: event];
}

- (void) rightMouseDragged: (NSEvent *) event {
    [self handleEvent: event];
}

- (void) otherMouseDown: (NSEvent *) event {
    [self handleEvent: event];
}

- (void) otherMouseUp: (NSEvent *) event {
    [self handleEvent: event];
}

- (void) otherMouseMoved: (NSEvent *) event {
    [self handleEvent: event];
}

- (void) otherMouseDragged: (NSEvent *) event {
    [self handleEvent: event];
}

- (void) scrollWheel: (NSEvent *) event {
    [self handleEvent: event];
}

- (void) keyDown: (NSEvent *) event {
    [self handleEvent: event];
}

- (void) keyUp: (NSEvent *) event {
    [self handleEvent: event];
}

- (void) flagsChanged: (NSEvent *) event {
    [self handleEvent: event];
}

- (BOOL) windowShouldClose: (id) sender {
    m_device->pushEvent(DeviceEvent(Device::EQuitEvent));
    return NO;
}

@end

MTS_NAMESPACE_BEGIN

NSGLDevice::NSGLDevice(NSGLSession *session)
 : Device(session), m_window(nil), m_view(nil), m_fmt(nil), m_currentContext(nil), m_visible(false), m_cursor(true)  {
    m_title = "Mitsuba [nsgl]";
}

NSGLDevice::~NSGLDevice() {
    Log(EDebug, "Destroying NSGL device");
    if (m_initialized)
        shutdown();
}

void NSGLDevice::init(Device *other) {
    Device::init(other);

    __mts_init_cocoa();

    NSOpenGLPixelFormatAttribute attribs[32];
    uint i=0;

    Device::init();

    m_currentContext = NULL;

    Log(EDebug, "Initializing NSGL device");

    NSRect contentRect = NSMakeRect(m_position.x, m_position.y,
        m_size.x, m_size.y);

    /* Protect the event queue */
    m_mutex = new Mutex();

    /* Create the device window */
    dispatch_async(dispatch_get_main_queue(), ^{ m_window = [[NSWindow alloc] initWithContentRect: contentRect
        styleMask: NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask
        backing: NSBackingStoreBuffered defer: NO]; 
		if (m_window == nil)
		Log(EError, "Could not create window");
		[m_window retain];

		if (m_center)
		[m_window center];

		/* Create a sub-view as drawing destination and in order to catch events */
		m_view = [[CustomView alloc] initWithFrame: contentRect];
		if (m_view == nil)
		Log(EError, "Could not create view");

		[m_view setDevice: this];
		[[m_window contentView] addSubview: m_view];
		[m_window setDelegate: m_view];
		[m_window setAcceptsMouseMovedEvents: YES];
        }); 

    /* Pixel format setup */
    AssertEx(m_redBits == m_blueBits || m_redBits == m_greenBits, "NSGL does not support individual color depths");

    attribs[i++] = NSOpenGLPFAColorSize;
    attribs[i++] = (NSOpenGLPixelFormatAttribute) m_redBits;

    attribs[i++] = NSOpenGLPFAAlphaSize;
    attribs[i++] = (NSOpenGLPixelFormatAttribute) m_alphaBits;

    attribs[i++] = NSOpenGLPFADepthSize;
    attribs[i++] = (NSOpenGLPixelFormatAttribute) m_depthBits;

    attribs[i++] = NSOpenGLPFAStencilSize;
    attribs[i++] = (NSOpenGLPixelFormatAttribute) m_stencilBits;

    if (m_doubleBuffer) {
        attribs[i++] = NSOpenGLPFADoubleBuffer;
    }

    if (m_fsaa > 1) {
        attribs[i++] = NSOpenGLPFASampleBuffers; attribs[i++] = (NSOpenGLPixelFormatAttribute) 1;
        attribs[i++] = NSOpenGLPFASamples; attribs[i++] = (NSOpenGLPixelFormatAttribute) m_fsaa;
    }

    attribs[i++] = NSOpenGLPFANoRecovery; // Never switch renderers
    attribs[i++] = NSOpenGLPFAWindow;
    attribs[i++] = (NSOpenGLPixelFormatAttribute) 0;
    attribs[i++] = (NSOpenGLPixelFormatAttribute) 0;

    m_fmt = [[NSOpenGLPixelFormat alloc] initWithAttributes: attribs];

    if (m_fmt == nil)
        Log(EError, "Could not create OpenGL pixel format!");

    m_initialized = true;
    setTitle(m_title);
}

void NSGLDevice::shutdown() {
    Device::shutdown();
    Log(EDebug, "Shutting down NSGL device");
    setVisible(false);
    [m_fmt release];
    [m_view release];
    [m_window release];
    m_initialized = false;
}


void NSGLDevice::setTitle(const std::string &title) {
    std::string finalTitle;

    Assert(m_initialized);
    if (m_showFPS && m_fps != 0) {
        finalTitle = formatString("%s - %i FPS", title.c_str(), m_fps);
    } else {
        finalTitle = title;
    }

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    NSString *string = [NSString stringWithUTF8String: finalTitle.c_str()];
    [m_window setTitle: string];
    Device::setTitle(title);

    [pool release];
}

void NSGLDevice::setPosition(const Point2i &position) {
    Assert(m_initialized);

    NSPoint point = NSMakePoint(position.x,
        CGDisplayPixelsHigh(kCGDirectMainDisplay) - position.y);

    [m_window setFrameTopLeftPoint: point];
    Device::setPosition(position);
}

void NSGLDevice::setVisible(bool visible) {
    Assert(m_initialized);

    if (visible && !m_visible) {
        [m_window makeKeyAndOrderFront: nil];
        m_visible = true;
    } else if (!visible && m_visible) {
        [m_window orderOut: nil];
        m_visible = false;
    }
}

void NSGLDevice::warpMouse(const Point2i &position) {
    Assert(m_initialized);

    NSRect frame = [m_window frame];

    CGPoint point;
    point.x = position.x + frame.origin.x;
    point.y = position.y + frame.origin.y;

    /* Avoids cursor freezing */
    [m_view ignoreNextMouseEvent];
    CGEventSourceRef evsrc =
        CGEventSourceCreate(kCGEventSourceStateCombinedSessionState);
    CGEventSourceSetLocalEventsSuppressionInterval(evsrc, 0.0);
    CGWarpMouseCursorPosition(point);
    CFRelease(evsrc);
}

void NSGLDevice::setGrab(bool grab) {
    Assert(m_initialized);

    if (grab) {
        warpMouse(Point2i(getSize().x / 2, getSize().y/2));
        CGAssociateMouseAndMouseCursorPosition(false);
    } else {
        CGAssociateMouseAndMouseCursorPosition(true);
    }
    [m_view ignoreFirstMouseMotion];
    showCursor(!grab);
}

void NSGLDevice::showCursor(bool enabled) {
    Assert(m_initialized);

    if (!m_cursor && enabled) {
        [NSCursor unhide];
        m_cursor = true;
    } else if (m_cursor && !enabled) {
        m_cursor = false;
        if (isMouseInWindow() && [m_view focused]) {
            [NSCursor hide];
        }
    }
}

void NSGLDevice::flip() {
    Assert(m_initialized);

    Device::flip();

    if (m_doubleBuffer) {
        Assert(m_currentContext != NULL);
        [m_currentContext flushBuffer];
    }
}

void NSGLDevice::pushEvent(const DeviceEvent &event) {
    m_mutex->lock();
    m_deviceEvents.push_back(event);
    m_mutex->unlock();
}

void NSGLDevice::processEvents() {
    Assert(m_initialized);

    m_mutex->lock();
    for (std::vector<DeviceEvent>::iterator it = m_deviceEvents.begin(); it!=m_deviceEvents.end(); ++it)
        fireDeviceEvent(*it);
    m_deviceEvents.clear();
    m_mutex->unlock();
}

bool NSGLDevice::isMouseInWindow() {
    return m_fullscreen || NSPointInRect([m_window mouseLocationOutsideOfEventStream], [m_view frame]);
}

void NSGLDevice::makeCurrent(Renderer *renderer) {
    Assert(m_initialized);

    m_currentContext = static_cast<NSOpenGLContext *>(static_cast<NSGLRenderer *>(renderer)->getContext());
    [m_currentContext setView: m_view];
    [m_currentContext makeCurrentContext];
}

MTS_IMPLEMENT_CLASS(NSGLDevice, false, Device)
MTS_NAMESPACE_END
