/*
 * Copyright (C) 2008 Apple Inc. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#if ENABLE(NETSCAPE_PLUGIN_API) && !defined(__LP64__)

#import "WebNetscapePluginEventHandlerCarbon.h"

#import "WebNetscapePluginView.h"
#import "WebKitLogging.h"
#import "WebKitSystemInterface.h"

// Send null events 50 times a second when active, so plug-ins like Flash get high frame rates.
#define NullEventIntervalActive         0.02
#define NullEventIntervalNotActive      0.25

WebNetscapePluginEventHandlerCarbon::WebNetscapePluginEventHandlerCarbon(WebNetscapePluginView* pluginView)
    : WebNetscapePluginEventHandler(pluginView)
    , m_keyEventHandler(0)
    , m_suspendKeyUpEvents(false)
{
}

static void getCarbonEvent(EventRecord* carbonEvent)
{
    carbonEvent->what = nullEvent;
    carbonEvent->message = 0;
    carbonEvent->when = TickCount();
    
    GetGlobalMouse(&carbonEvent->where);
    carbonEvent->modifiers = GetCurrentKeyModifiers();
    if (!Button())
        carbonEvent->modifiers |= btnState;
}

static EventModifiers modifiersForEvent(NSEvent *event)
{
    EventModifiers modifiers;
    unsigned int modifierFlags = [event modifierFlags];
    NSEventType eventType = [event type];
    
    modifiers = 0;
    
    if (eventType != NSLeftMouseDown && eventType != NSRightMouseDown)
        modifiers |= btnState;
    
    if (modifierFlags & NSCommandKeyMask)
        modifiers |= cmdKey;
    
    if (modifierFlags & NSShiftKeyMask)
        modifiers |= shiftKey;

    if (modifierFlags & NSAlphaShiftKeyMask)
        modifiers |= alphaLock;

    if (modifierFlags & NSAlternateKeyMask)
        modifiers |= optionKey;

    if (modifierFlags & NSControlKeyMask || eventType == NSRightMouseDown)
        modifiers |= controlKey;
    
    return modifiers;
}

static void getCarbonEvent(EventRecord *carbonEvent, NSEvent *cocoaEvent)
{
    if (WKConvertNSEventToCarbonEvent(carbonEvent, cocoaEvent))
        return;
    
    NSPoint where = [[cocoaEvent window] convertBaseToScreen:[cocoaEvent locationInWindow]];
        
    carbonEvent->what = nullEvent;
    carbonEvent->message = 0;
    carbonEvent->when = (UInt32)([cocoaEvent timestamp] * 60); // seconds to ticks
    carbonEvent->where.h = (short)where.x;
    carbonEvent->where.v = (short)(NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]) - where.y);
    carbonEvent->modifiers = modifiersForEvent(cocoaEvent);
}

void WebNetscapePluginEventHandlerCarbon::sendNullEvent()
{
    EventRecord event;
    
    getCarbonEvent(&event);
    
    // Plug-in should not react to cursor position when not active or when a menu is down.
    MenuTrackingData trackingData;
    OSStatus error = GetMenuTrackingData(NULL, &trackingData);
    
    // Plug-in should not react to cursor position when the actual window is not key.
    if (![[m_pluginView window] isKeyWindow] || (error == noErr && trackingData.menu)) {
        // FIXME: Does passing a v and h of -1 really prevent it from reacting to the cursor position?
        event.where.v = -1;
        event.where.h = -1;
    }
    
    sendEvent(&event);
}

void WebNetscapePluginEventHandlerCarbon::drawRect(CGContextRef, const NSRect&)
{
    EventRecord event;
    
    getCarbonEvent(&event);
    event.what = updateEvt;
    WindowRef windowRef = (WindowRef)[[m_pluginView window] windowRef];
    event.message = (unsigned long)windowRef;
    
    BOOL acceptedEvent;
    acceptedEvent = sendEvent(&event);
    
    LOG(PluginEvents, "NPP_HandleEvent(updateEvt): %d", acceptedEvent);
}

void WebNetscapePluginEventHandlerCarbon::mouseDown(NSEvent* theEvent)
{
    EventRecord event;
    
    getCarbonEvent(&event, theEvent);
    event.what = ::mouseDown;
    
    BOOL acceptedEvent;
    acceptedEvent = sendEvent(&event);
    
    LOG(PluginEvents, "NPP_HandleEvent(mouseDown): %d pt.v=%d, pt.h=%d", acceptedEvent, event.where.v, event.where.h);    
}

void WebNetscapePluginEventHandlerCarbon::mouseUp(NSEvent* theEvent)
{
    EventRecord event;
    
    getCarbonEvent(&event, theEvent);
    event.what = ::mouseUp;
    
    BOOL acceptedEvent;
    acceptedEvent = sendEvent(&event);
    
    LOG(PluginEvents, "NPP_HandleEvent(mouseUp): %d pt.v=%d, pt.h=%d", acceptedEvent, event.where.v, event.where.h);    
}

bool WebNetscapePluginEventHandlerCarbon::scrollWheel(NSEvent* theEvent)
{
    return false;
}

void WebNetscapePluginEventHandlerCarbon::mouseEntered(NSEvent* theEvent)
{
    EventRecord event;
    
    getCarbonEvent(&event, theEvent);
    event.what = NPEventType_AdjustCursorEvent;
    
    BOOL acceptedEvent;
    acceptedEvent = sendEvent(&event);
    
    LOG(PluginEvents, "NPP_HandleEvent(mouseEntered): %d", acceptedEvent);    
}

void WebNetscapePluginEventHandlerCarbon::mouseExited(NSEvent* theEvent)
{
    EventRecord event;
    
    getCarbonEvent(&event, theEvent);
    event.what = NPEventType_AdjustCursorEvent;
    
    BOOL acceptedEvent;
    acceptedEvent = sendEvent(&event);
    
    LOG(PluginEvents, "NPP_HandleEvent(mouseExited): %d", acceptedEvent);    
}

void WebNetscapePluginEventHandlerCarbon::mouseDragged(NSEvent*)
{
}

void WebNetscapePluginEventHandlerCarbon::mouseMoved(NSEvent* theEvent)
{
    EventRecord event;
    
    getCarbonEvent(&event, theEvent);
    event.what = NPEventType_AdjustCursorEvent;
    
    BOOL acceptedEvent;
    acceptedEvent = sendEvent(&event);
    
    LOG(PluginEvents, "NPP_HandleEvent(mouseMoved): %d", acceptedEvent);
}

void WebNetscapePluginEventHandlerCarbon::keyDown(NSEvent *theEvent)
{
    m_suspendKeyUpEvents = true;
    WKSendKeyEventToTSM(theEvent);
}

void WebNetscapePluginEventHandlerCarbon::syntheticKeyDownWithCommandModifier(int keyCode, char character)
{
    EventRecord event;
    getCarbonEvent(&event);
    
    event.what = ::keyDown;
    event.modifiers |= cmdKey;
    event.message = keyCode << 8 | character;
    sendEvent(&event);
}

static UInt32 keyMessageForEvent(NSEvent *event)
{
    NSData *data = [[event characters] dataUsingEncoding:CFStringConvertEncodingToNSStringEncoding(CFStringGetSystemEncoding())];
    if (!data)
        return 0;

    UInt8 characterCode;
    [data getBytes:&characterCode length:1];
    UInt16 keyCode = [event keyCode];
    return keyCode << 8 | characterCode;
}    
    
void WebNetscapePluginEventHandlerCarbon::keyUp(NSEvent* theEvent)
{
    WKSendKeyEventToTSM(theEvent);
    
    // TSM won't send keyUp events so we have to send them ourselves.
    // Only send keyUp events after we receive the TSM callback because this is what plug-in expect from OS 9.
    if (!m_suspendKeyUpEvents) {
        EventRecord event;
        
        getCarbonEvent(&event, theEvent);
        event.what = ::keyUp;
        
        if (event.message == 0)
            event.message = keyMessageForEvent(theEvent);
        
        sendEvent(&event);
    }    
}

void WebNetscapePluginEventHandlerCarbon::flagsChanged(NSEvent*)
{
}

void WebNetscapePluginEventHandlerCarbon::focusChanged(bool hasFocus)
{
    EventRecord event;
    
    getCarbonEvent(&event);
    bool acceptedEvent;
    if (hasFocus) {
        event.what = NPEventType_GetFocusEvent;
        acceptedEvent = sendEvent(&event);
        LOG(PluginEvents, "NPP_HandleEvent(NPEventType_GetFocusEvent): %d", acceptedEvent);
        installKeyEventHandler();
    } else {
        event.what = NPEventType_LoseFocusEvent;
        acceptedEvent = sendEvent(&event);
        LOG(PluginEvents, "NPP_HandleEvent(NPEventType_LoseFocusEvent): %d", acceptedEvent);
        removeKeyEventHandler();
    }
}

void WebNetscapePluginEventHandlerCarbon::windowFocusChanged(bool hasFocus)
{
    WindowRef windowRef = (WindowRef)[[m_pluginView window] windowRef];

    SetUserFocusWindow(windowRef);
    
    EventRecord event;
    
    getCarbonEvent(&event);
    event.what = activateEvt;
    event.message = (unsigned long)windowRef;
    if (hasFocus)
        event.modifiers |= activeFlag;
    
    BOOL acceptedEvent;
    acceptedEvent = sendEvent(&event);
    
    LOG(PluginEvents, "NPP_HandleEvent(activateEvent): %d  isActive: %d", acceptedEvent, hasFocus);    
}

OSStatus WebNetscapePluginEventHandlerCarbon::TSMEventHandler(EventHandlerCallRef inHandlerRef, EventRef inEvent, void *eventHandler)
{    
    EventRef rawKeyEventRef;
    OSStatus status = GetEventParameter(inEvent, kEventParamTextInputSendKeyboardEvent, typeEventRef, NULL, sizeof(EventRef), NULL, &rawKeyEventRef);
    if (status != noErr) {
        LOG_ERROR("GetEventParameter failed with error: %d", status);
        return noErr;
    }
    
    // Two-pass read to allocate/extract Mac charCodes
    ByteCount numBytes;    
    status = GetEventParameter(rawKeyEventRef, kEventParamKeyMacCharCodes, typeChar, NULL, 0, &numBytes, NULL);
    if (status != noErr) {
        LOG_ERROR("GetEventParameter failed with error: %d", status);
        return noErr;
    }
    char *buffer = (char *)malloc(numBytes);
    status = GetEventParameter(rawKeyEventRef, kEventParamKeyMacCharCodes, typeChar, NULL, numBytes, NULL, buffer);
    if (status != noErr) {
        LOG_ERROR("GetEventParameter failed with error: %d", status);
        free(buffer);
        return noErr;
    }
    
    EventRef cloneEvent = CopyEvent(rawKeyEventRef);
    unsigned i;
    for (i = 0; i < numBytes; i++) {
        status = SetEventParameter(cloneEvent, kEventParamKeyMacCharCodes, typeChar, 1 /* one char code */, &buffer[i]);
        if (status != noErr) {
            LOG_ERROR("SetEventParameter failed with error: %d", status);
            free(buffer);
            return noErr;
        }
        
        EventRecord eventRec;
        if (ConvertEventRefToEventRecord(cloneEvent, &eventRec)) {
            BOOL acceptedEvent;
            acceptedEvent = static_cast<WebNetscapePluginEventHandlerCarbon*>(eventHandler)->sendEvent(&eventRec);
            
            LOG(PluginEvents, "NPP_HandleEvent(keyDown): %d charCode:%c keyCode:%lu",
                acceptedEvent, (char) (eventRec.message & charCodeMask), (eventRec.message & keyCodeMask));
            
            // We originally thought that if the plug-in didn't accept this event,
            // we should pass it along so that keyboard scrolling, for example, will work.
            // In practice, this is not a good idea, because plug-ins tend to eat the event but return false.
            // MacIE handles each key event twice because of this, but we will emulate the other browsers instead.
        }
    }
    ReleaseEvent(cloneEvent);
    
    free(buffer);
    
    return noErr;
}

void WebNetscapePluginEventHandlerCarbon::installKeyEventHandler()
{
    static const EventTypeSpec sTSMEvents[] =
    {
        { kEventClassTextInput, kEventTextInputUnicodeForKeyEvent }
    };
    
    if (!m_keyEventHandler) {
        InstallEventHandler(GetWindowEventTarget((WindowRef)[[m_pluginView window] windowRef]),
                            NewEventHandlerUPP(TSMEventHandler),
                            GetEventTypeCount(sTSMEvents),
                            sTSMEvents,
                            this,
                            &m_keyEventHandler);
    }
}

void WebNetscapePluginEventHandlerCarbon::removeKeyEventHandler()
{
    if (m_keyEventHandler) {
        RemoveEventHandler(m_keyEventHandler);
        m_keyEventHandler = 0;
    }    
}

void WebNetscapePluginEventHandlerCarbon::nullEventTimerFired(CFRunLoopTimerRef timerRef, void *context)
{
    static_cast<WebNetscapePluginEventHandlerCarbon*>(context)->sendNullEvent();
}

void WebNetscapePluginEventHandlerCarbon::startTimers(bool throttleTimers)
{
    ASSERT(!m_nullEventTimer);
    
    CFTimeInterval interval = !throttleTimers ? NullEventIntervalActive : NullEventIntervalNotActive;    
    
    CFRunLoopTimerContext context = { 0, this, NULL, NULL, NULL };
    m_nullEventTimer.adoptCF(CFRunLoopTimerCreate(0, CFAbsoluteTimeGetCurrent() + interval, interval,
                                                   0, 0, nullEventTimerFired, &context));
    CFRunLoopAddTimer(CFRunLoopGetCurrent(), m_nullEventTimer.get(), kCFRunLoopDefaultMode);
}

void WebNetscapePluginEventHandlerCarbon::stopTimers()
{
    if (!m_nullEventTimer)
        return;
    
    CFRunLoopTimerInvalidate(m_nullEventTimer.get());
    m_nullEventTimer = 0;
}

void* WebNetscapePluginEventHandlerCarbon::platformWindow(NSWindow* window)
{
    return [window windowRef];
}

bool WebNetscapePluginEventHandlerCarbon::sendEvent(EventRecord* event)
{
    // If at any point the user clicks or presses a key from within a plugin, set the 
    // currentEventIsUserGesture flag to true. This is important to differentiate legitimate 
    // window.open() calls;  we still want to allow those.  See rdar://problem/4010765
    if (event->what == ::mouseDown || event->what == ::keyDown || event->what == ::mouseUp || event->what == ::autoKey)
        m_currentEventIsUserGesture = true;
    
    m_suspendKeyUpEvents = false; 

    bool result = [m_pluginView sendEvent:event isDrawRect:event->what == updateEvt];
    
    m_currentEventIsUserGesture = false;
    
    return result;
}

#endif // ENABLE(NETSCAPE_PLUGIN_API) && !defined(__LP64__)