/**********************************************************************
 
 Audacity: A Digital Audio Editor
 
 KeyboardCapture.cpp
 
 Paul Licameli split this from Project.cpp
 
 **********************************************************************/

#include "KeyboardCapture.h"

#if defined(__WXMAC__)
#include "CFResources.h"
#include <wx/textctrl.h>
#include <AppKit/AppKit.h>
#include <wx/osx/core/private.h>
#include <wx/osx/cocoa/private.h>
#elif defined(__WXGTK__)
#include <gtk/gtk.h>
#endif

#include <wx/app.h>
#include <wx/button.h>
#include <wx/eventfilter.h>
#include <wx/toplevel.h>
#include <wx/weakref.h>
#include <wx/window.h>

#include "AudacityException.h"

////////////////////////////////////////////////////////////
/// Custom events
////////////////////////////////////////////////////////////
DEFINE_EVENT_TYPE(EVT_CAPTURE_KEY);

namespace {

wxWindowRef &sHandler()
{
   static wxWindowRef theHandler;
   return theHandler;
}

}

namespace KeyboardCapture
{

// Keyboard capture
bool IsHandler(const wxWindow *handler)
{
   return GetHandler() == handler;
}

wxWindow *GetHandler()
{
   return sHandler();
}

void Capture(wxWindow *handler)
{
   sHandler() = handler;
}

void Release(wxWindow *handler)
{
//   wxASSERT(sHandler() == handler);
   sHandler() = nullptr;
}

void OnFocus( wxWindow &window, wxFocusEvent &event )
{
   if (event.GetEventType() == wxEVT_KILL_FOCUS)
      KeyboardCapture::Release( &window );
   else
      KeyboardCapture::Capture( &window );

   window.Refresh( false );
   event.Skip();
}

}

// Shared by all projects
static class EventMonitor final : public wxEventFilter
{
public:
   EventMonitor()
   :  wxEventFilter()
   {
#if defined(__WXMAC__)
      // In wx3, the menu accelerators take precedence over key event processing
      // so we won't get wxEVT_CHAR_HOOK events for combinations assigned to menus.
      // Since we only support OS X 10.6 or greater, we can use an event monitor
      // to capture the key event before it gets to the normal wx3 processing.

      // The documentation for addLocalMonitorForEventsMatchingMask implies that
      // NSKeyUpMask can't be used in 10.6, but testing shows that it can.
      NSEventMask mask = NSKeyDownMask | NSKeyUpMask;

      mHandler =
      [
         NSEvent addLocalMonitorForEventsMatchingMask:mask handler:^NSEvent *(NSEvent *event)
         {
            WXWidget widget = (WXWidget) [ [event window] firstResponder];
            if (widget)
            {
               wxWidgetCocoaImpl *impl = (wxWidgetCocoaImpl *)
                  wxWidgetImpl::FindFromWXWidget(widget);
               if (impl)
               {
                  mEvent = event;

                  wxKeyEvent wxevent([event type] == NSKeyDown ? wxEVT_KEY_DOWN : wxEVT_KEY_UP);
                  impl->SetupKeyEvent(wxevent, event);

                  NSEvent *result;
                  if ([event type] == NSKeyDown)
                  {
                     wxKeyEvent eventHook(wxEVT_CHAR_HOOK, wxevent);
                     result = FilterEvent(eventHook) == Event_Processed ? nil : event;
                  }
                  else
                  {
                     result = FilterEvent(wxevent) == Event_Processed ? nil : event;
                  }

                  mEvent = nullptr;
                  return result;
               }
            }

            return event;
         }
      ];

      // Bug1252: must also install this filter with wxWidgets, else
      // we don't intercept command keys when focus is in a combo box.
      wxEvtHandler::AddFilter(this);
#else

      wxEvtHandler::AddFilter(this);

#endif
   }

   ~EventMonitor() override
   {
#if defined(__WXMAC__)
      wxEvtHandler::RemoveFilter(this);
      [NSEvent removeMonitor:mHandler];
#else
      wxEvtHandler::RemoveFilter(this);
#endif
   }

   int FilterEvent(wxEvent& event) override
   {
      // Unguarded exception propagation may crash the program, at least
      // on Mac while in the objective-C closure above
      return GuardedCall< int > ( [&] {
         // Quickly bail if this isn't something we want.
         wxEventType type = event.GetEventType();
         if (type != wxEVT_CHAR_HOOK && type != wxEVT_KEY_UP)
         {
            return Event_Skip;
         }

         wxKeyEvent key = static_cast<wxKeyEvent &>( event );

         if ( !( KeyboardCapture::PreFilter::Call(key) ) )
            return Event_Skip;

#ifdef __WXMAC__
         // Bugs 1329 and 2107 (Mac only)
         // wxButton::SetDefault() alone doesn't cause correct event routing
         // of key-down to the button when a text entry or combo has focus,
         // but we can intercept wxEVT_CHAR_HOOK here and do it
         if ( type == wxEVT_CHAR_HOOK &&
            key.GetKeyCode() == WXK_RETURN ) {
            const auto focus = wxWindow::FindFocus();
            // Bug 2267 (Mac only): don't apply fix for 2107 when a text entry
            // needs to allow multiple line input
            const auto text = dynamic_cast<wxTextCtrl*>(focus);
            if ( !(text && text->IsMultiLine()) ) {
               if (auto top =
                  dynamic_cast< wxTopLevelWindow* >(
                     wxGetTopLevelParent( focus ) ) ) {
                  if ( auto button =
                     dynamic_cast<wxButton*>( top->GetDefaultItem() ) ) {
                     wxCommandEvent newEvent{ wxEVT_BUTTON, button->GetId() };
                     newEvent.SetEventObject( button );
                     button->GetEventHandler()->AddPendingEvent( newEvent );
                     return Event_Processed;
                  }
               }
            }
         }
#endif

         // Make a copy of the event and (possibly) make it look like a key down
         // event.
         if (type == wxEVT_CHAR_HOOK)
         {
            key.SetEventType(wxEVT_KEY_DOWN);
         }

         // Give the capture handler first dibs at the event.
         wxWindow *handler = KeyboardCapture::GetHandler();
         if (handler && HandleCapture(handler, key))
         {
            return Event_Processed;
         }

         if ( KeyboardCapture::PostFilter::Call( key ) )
            return Event_Processed;

         // Give it back to WX for normal processing.
         return Event_Skip;
      },
      // Immediate handler invokes the same high level catch-all as for
      // unhandled exceptions, which will also do its own delayed handling
      [](AudacityException *pEx){
         if (pEx)
            wxTheApp->OnExceptionInMainLoop();
         else
            throw;
         return Event_Processed;
      },
      // So don't duplicate delayed handling:
      [](auto){}
      );
   }

private:

   // Returns true if the event was captured and processed
   bool HandleCapture(wxWindow *target, const wxKeyEvent & event)
   {
      if (wxGetTopLevelParent(target) != wxGetTopLevelParent(wxWindow::FindFocus()))
      {
         return false;
      }
      wxEvtHandler *handler = target->GetEventHandler();

      // We make a copy of the event because the capture handler may modify it.
      wxKeyEvent temp = event;

#if defined(__WXGTK__)
      // wxGTK uses the control and alt modifiers to represent ALTGR,
      // so remove it as it might confuse the capture handlers.
      if (temp.GetModifiers() == (wxMOD_CONTROL | wxMOD_ALT))
      {
         temp.SetControlDown(false);
         temp.SetAltDown(false);
      }
#endif

      // Ask the capture handler if the key down/up event is something it
      // might be interested in handling.
      wxCommandEvent e(EVT_CAPTURE_KEY);
      e.SetEventObject(&temp);
      e.StopPropagation();
      if (!handler->ProcessEvent(e))
      {
         return false;
      }

      // Now, let the handler process the normal key event.
      bool keyDown = temp.GetEventType() == wxEVT_KEY_DOWN;
      temp.WasProcessed();
      temp.StopPropagation();
      wxEventProcessInHandlerOnly onlyDown(temp, handler);
      bool processed = handler->ProcessEvent(temp);

      // Don't go any further if the capture handler didn't process
      // the key down event.
      if (!processed && keyDown)
      {
         return false;
      }

      // At this point the capture handler has either processed a key down event
      // or we're dealing with a key up event.
      //
      // So, only generate the char events for key down events.
      if (keyDown)
      {
         wxString chars = GetUnicodeString(temp);
         for (size_t i = 0, cnt = chars.length(); i < cnt; i++)
         {
            temp = event;
            temp.SetEventType(wxEVT_CHAR);
            temp.WasProcessed();
            temp.StopPropagation();
            temp.m_uniChar = chars[i];
            wxEventProcessInHandlerOnly onlyChar(temp, handler);
            handler->ProcessEvent(temp);
         }
      }

      // We get here for processed key down events or for key up events, whether
      // processed or not.
      return true;
   }

   // Convert the key down event to a unicode string.
   wxString GetUnicodeString(const wxKeyEvent & event)
   {
      wxString chars;

#if defined(__WXMSW__)

      BYTE ks[256];
      GetKeyboardState(ks);
      WCHAR ucode[256];
      int res = ToUnicode(event.GetRawKeyCode(), 0, ks, ucode, 256, 0);
      if (res >= 1)
      {
         chars.Append(ucode, res);
      }

#elif defined(__WXGTK__)

      chars.Append((wxChar) gdk_keyval_to_unicode(event.GetRawKeyCode()));

#elif defined(__WXMAC__)

      if (!mEvent) {
         // TODO:  we got here without getting the NSEvent pointer,
         // as in the combo box case of bug 1252.  We can't compute it!
         // This makes a difference only when there is a capture handler.
         // It's never the case yet that there is one.

         // Return just a one-character string.
         return event.GetUnicodeKey();
      }

      NSString *c = [mEvent charactersIgnoringModifiers];
      if ([c length] == 1)
      {
         unichar codepoint = [c characterAtIndex:0];
         if ((codepoint >= 0xF700 && codepoint <= 0xF8FF) || codepoint == 0x7F)
         {
            return chars;
         }
      }

      c = [mEvent characters];
      chars = [c UTF8String];

      auto uchr = static_cast<CFDataRef>(TISGetInputSourceProperty(
         CF_ptr<TISInputSourceRef>{ TISCopyCurrentKeyboardInputSource() }
            .get(),
         kTISPropertyUnicodeKeyLayoutData));
      if (!uchr)
         return chars;

      const UCKeyboardLayout *keyboardLayout = (const UCKeyboardLayout*)CFDataGetBytePtr(uchr);
      if (keyboardLayout == NULL)
      {
         return chars;
      }

      const UniCharCount maxStringLength = 255;
      UniCharCount actualStringLength = 0;
      UniChar unicodeString[maxStringLength];
      UInt32 nsflags = [mEvent modifierFlags];
      UInt16 modifiers = (nsflags & NSAlphaShiftKeyMask ? alphaLock : 0) |
                         (nsflags & NSShiftKeyMask ? shiftKey : 0) |
                         (nsflags & NSControlKeyMask ? controlKey : 0) |
                         (nsflags & NSAlternateKeyMask ? optionKey : 0) |
                         (nsflags & NSCommandKeyMask ? cmdKey : 0);

      OSStatus status = UCKeyTranslate(keyboardLayout,
                                       [mEvent keyCode],
                                       kUCKeyActionDown,
                                       (modifiers >> 8) & 0xff,
                                       LMGetKbdType(),
                                       0,
                                       &mDeadKeyState,
                                       maxStringLength,
                                       &actualStringLength,
                                       unicodeString);

      if (status != noErr)
      {
         return chars;
      }

      chars = [ [NSString stringWithCharacters:unicodeString
                                       length:(NSInteger)actualStringLength] UTF8String];

#endif

      return chars;
   }

private:

#if defined(__WXMAC__)
   id mHandler;
   NSEvent *mEvent {};
   UInt32 mDeadKeyState;
#endif

} monitor;
