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

  Audacity: A Digital Audio Editor

  AButton.cpp

  Dominic Mazzoni

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

\class AButton
\brief A wxButton with mouse-over behaviour.

  AButton is a custom button class for Audacity.  The main feature it
  supports that a wxButton does not is mouseovers.

  It uses an image for all of its states: up, over, down, and
  disabled, allowing any sort of customization you want.  Currently
  it does not support transparency effects, so the image musts be
  rectangular and opaque.

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

#include "../Audacity.h"

#include "AButton.h"
#include "../AColor.h"

#include <wx/app.h>
#include <wx/dcclient.h>
#include <wx/dcmemory.h>
#include <wx/dcbuffer.h>
#include <wx/eventfilter.h>
#include <wx/image.h>
#include <wx/timer.h>

//This is needed for tooltips
#include "../Project.h"
#include <wx/tooltip.h>

BEGIN_EVENT_TABLE(AButton, wxWindow)
   EVT_MOUSE_EVENTS(AButton::OnMouseEvent)
   EVT_MOUSE_CAPTURE_LOST(AButton::OnCaptureLost)
   EVT_KEY_DOWN(AButton::OnKeyDown)
   EVT_SET_FOCUS(AButton::OnSetFocus)
   EVT_KILL_FOCUS(AButton::OnKillFocus)
   EVT_PAINT(AButton::OnPaint)
   EVT_SIZE(AButton::OnSize)
   EVT_ERASE_BACKGROUND(AButton::OnErase)
END_EVENT_TABLE()

// LL:  An alternative to this might be to just use the wxEVT_KILL_FOCUS
//      or wxEVT_ACTIVATE events.
class AButton::Listener final
   : public wxEventFilter
{
public:
   Listener (AButton *button);
   ~Listener();

   int FilterEvent(wxEvent &event) override;

   void OnEvent();

private:
   AButton *mButton;
};

AButton::Listener::Listener (AButton *button)
: mButton(button)
{
   wxEvtHandler::AddFilter(this);
}

AButton::Listener::~Listener ()
{
   wxEvtHandler::RemoveFilter(this);
}

void AButton::Listener::OnEvent()
{
   if (!mButton->IsDown())
   {
      int idx = 0;
      // Ignore the event, consult key states.  One modifier key might
      // have gone up but another remained down.
      // Note that CMD (or CTRL) takes precedence over Shift if both are down
      // and alternates are defined for both
      // see also AButton::OnMouseEvent()
      if (wxGetKeyState(WXK_CONTROL) && mButton->HasAlternateImages(2))
         idx = 2;
      else if (wxGetKeyState(WXK_SHIFT) && mButton->HasAlternateImages(1))
         idx = 1;

      // Turn e.g. the "Play" button into a "Loop" button
      // or "Cut Preview" button
      mButton->SetAlternateIdx(idx);
   }
}

int AButton::Listener::FilterEvent(wxEvent &event)
{
   if (event.GetEventType() == wxEVT_KEY_DOWN ||
       event.GetEventType() == wxEVT_KEY_UP)
      OnEvent();
   else if (event.GetEventType() == wxEVT_SET_FOCUS)
      // A modal dialog might have eaten the modifier key-up with its own
      // filter before we saw it; this is adequate to fix the button image
      // when the dialog disappears.
      OnEvent();
   return Event_Skip;
}

AButton::AButton(wxWindow * parent,
                 wxWindowID id,
                 const wxPoint & pos,
                 const wxSize & size,
                 wxImage up,
                 wxImage over,
                 wxImage down,
                 wxImage dis,
                 bool toggle):
   wxWindow()
{
   Init(parent, id, pos, size,
        ImageRoll(up), ImageRoll(over),
        ImageRoll(down), ImageRoll(dis),
        toggle);
}

AButton::AButton(wxWindow * parent,
                 wxWindowID id,
                 const wxPoint & pos,
                 const wxSize & size,
                 ImageRoll up,
                 ImageRoll over,
                 ImageRoll down,
                 ImageRoll dis,
                 bool toggle):
   wxWindow()
{
   Init(parent, id, pos, size,
        up, over, down, dis,
        toggle);
}

AButton::~AButton()
{
   if(HasCapture())
      ReleaseMouse();
}

void AButton::Init(wxWindow * parent,
                   wxWindowID id,
                   const wxPoint & pos,
                   const wxSize & size,
                   ImageRoll up,
                   ImageRoll over,
                   ImageRoll down,
                   ImageRoll dis,
                   bool toggle)
{
   // Bug in wxWidgets 2.8.12: by default pressing Enter on an AButton is interpreted as
   // a navigation event - move to next control. As a workaround, the style wxWANTS_CHARS
   // results in all characters being available in the OnKeyDown function below. Note
   // that OnKeyDown now has to handle navigation.
   Create(parent, id, pos, size, wxWANTS_CHARS);

   mWasShiftDown = false;
   mWasControlDown = false;
   mButtonIsDown = false;
   mIsClicking = false;
   mEnabled = true;
   mCursorIsInWindow = false;
   mToggle = toggle;
   mUseDisabledAsDownHiliteImage = false;

   mImages.resize(1);
   mImages[0].mArr[0] = up;
   mImages[0].mArr[1] = over;
   mImages[0].mArr[2] = down;
   mImages[0].mArr[3] = dis;

   mAlternateIdx = 0;

   mFocusRect = GetClientRect().Deflate( 3, 3 );
   mForceFocusRect = false;

   SetSizeHints(mImages[0].mArr[0].GetMinSize(),
                mImages[0].mArr[0].GetMaxSize());

#if wxUSE_ACCESSIBILITY
   SetName( wxT("") );
   SetAccessible(safenew AButtonAx(this));
#endif
}

void AButton::UseDisabledAsDownHiliteImage(bool flag)
{
   mUseDisabledAsDownHiliteImage = flag;
}

// This compensates for a but in wxWidgets 3.0.2 for mac:
// Couldn't set focus from keyboard when AcceptsFocus returns false;
// this bypasses that limitation
void AButton::SetFocusFromKbd()
{
   auto temp = TemporarilyAllowFocus();
   SetFocus();
}

void AButton::SetAlternateImages(unsigned idx,
                                 wxImage up,
                                 wxImage over,
                                 wxImage down,
                                 wxImage dis)
{
   if (1 + idx > mImages.size())
      mImages.resize(1 + idx);
   mImages[idx].mArr[0] = ImageRoll(up);
   mImages[idx].mArr[1] = ImageRoll(over);
   mImages[idx].mArr[2] = ImageRoll(down);
   mImages[idx].mArr[3] = ImageRoll(dis);
}

void AButton::SetAlternateImages(unsigned idx,
                                 ImageRoll up,
                                 ImageRoll over,
                                 ImageRoll down,
                                 ImageRoll dis)
{
   if (1 + idx > mImages.size())
      mImages.resize(1 + idx);
   mImages[idx].mArr[0] = up;
   mImages[idx].mArr[1] = over;
   mImages[idx].mArr[2] = down;
   mImages[idx].mArr[3] = dis;
}

void AButton::SetAlternateIdx(unsigned idx)
{
   // If alternate-image-state is already correct then
   // nothing to do (saves repainting button).
   if( mAlternateIdx == idx )
      return;
   mAlternateIdx = idx;
   Refresh(false);
}

void AButton::FollowModifierKeys()
{
   if(!mListener)
      mListener = std::make_unique<Listener>(this);
}

void AButton::SetFocusRect(wxRect & r)
{
   mFocusRect = r;
   mForceFocusRect = true;
}

AButton::AButtonState AButton::GetState()
{
   AButtonState state;

   if (!mEnabled && (!mToggle || !mButtonIsDown))
      return AButtonDis;

   if (mCursorIsInWindow) {
      if (mToggle) {
         if (mIsClicking) {
            state = mButtonIsDown ? AButtonUp : AButtonDown;
            if (mUseDisabledAsDownHiliteImage) {
               state = mButtonIsDown ? AButtonOver : AButtonDis;
            }
         }
         else {
            state = mButtonIsDown ? AButtonDown : AButtonOver;
            if (mUseDisabledAsDownHiliteImage) {
               state = mButtonIsDown ? AButtonDis : AButtonOver;
            }
         }
      }
      else {
         if (mIsClicking) {
            state = mButtonIsDown ? AButtonOver : AButtonDown;
         }
         else {
            state = mButtonIsDown ? AButtonDown : AButtonOver;
         }
      }
   }
   else {
      if (mToggle) {
         state = mButtonIsDown ? AButtonDown : AButtonUp;
      }
      else {
         state = mButtonIsDown ? AButtonDown : AButtonUp;
      }
   }

   return state;
}

void AButton::OnPaint(wxPaintEvent & WXUNUSED(event))
{
   wxBufferedPaintDC dc(this);

   AButtonState buttonState = GetState();

   mImages[mAlternateIdx].mArr[buttonState].Draw(dc, GetClientRect());

   if( this == wxWindow::FindFocus() )
   {
      AColor::DrawFocus( dc, mFocusRect );
   }
}

void AButton::OnErase(wxEraseEvent & WXUNUSED(event))
{
   // Ignore it to prevent flashing
}

void AButton::OnSize(wxSizeEvent & WXUNUSED(event))
{
   if (!mForceFocusRect)
   {
      mFocusRect = GetClientRect().Deflate( 3, 3 );
   }
   Refresh(false);
}

bool AButton::s_AcceptsFocus{ false };

bool AButton::HasAlternateImages(unsigned idx)
{
   if (mImages.size() <= idx)
      return false;

   const ImageArr &images = mImages[idx];
   const ImageRoll (&arr)[4] = images.mArr;
   return (arr[0].Ok() &&
           arr[1].Ok() &&
           arr[2].Ok() &&
           arr[3].Ok());
}

void AButton::OnMouseEvent(wxMouseEvent & event)
{
   wxSize clientSize = GetClientSize();
   AButtonState prevState = GetState();

   if (event.Entering()) {
      // Bug 1201:  On Mac, unsetting and re-setting the tooltip may be needed
      // to make it pop up when we want it.
      auto text = GetToolTipText();
      UnsetToolTip();
      SetToolTip(text);
      mCursorIsInWindow = true;
   }
   else if (event.Leaving())
      mCursorIsInWindow = false;
   else
      mCursorIsInWindow =
         (event.m_x >= 0 && event.m_y >= 0 &&
          event.m_x < clientSize.x && event.m_y < clientSize.y);

   if (mEnabled && event.IsButton()) {
      if (event.ButtonIsDown(wxMOUSE_BTN_ANY)) {
         mIsClicking = true;
         if (event.ButtonDClick())
            mIsDoubleClicked = true;
         if( !HasCapture() )
            CaptureMouse();
      }
      else if (mIsClicking) {
         mIsClicking = false;

         if (HasCapture())
            ReleaseMouse();

         if (mCursorIsInWindow && (mToggle || !mButtonIsDown)) {
            if (mToggle)
               mButtonIsDown = !mButtonIsDown;
            else
               mButtonIsDown = true;

            mWasShiftDown = event.ShiftDown();
            mWasControlDown = event.ControlDown();

            Click();
         }
      }
   }

   // Only redraw and change tooltips if the state has changed.
   AButtonState newState = GetState();

   if (newState != prevState) {
      Refresh(false);

      if (mCursorIsInWindow)
         UpdateStatus();
      else {
         GetActiveProject()->TP_DisplayStatusMessage(wxT(""));
      }
   }
   else
      event.Skip();
}

void AButton::UpdateStatus()
{
   if (mCursorIsInWindow) {
#if wxUSE_TOOLTIPS // Not available in wxX11
      // Display the tooltip in the status bar
      wxToolTip * pTip = this->GetToolTip();
      if( pTip ) {
         wxString tipText = pTip->GetTip();
         if (!mEnabled)
            tipText += _(" (disabled)");
         GetActiveProject()->TP_DisplayStatusMessage(tipText);
      }
#endif
   }
}

void AButton::OnCaptureLost(wxMouseCaptureLostEvent & WXUNUSED(event))
{
   wxMouseEvent e(wxEVT_LEFT_UP);
   e.m_x = -1;
   e.m_y = -1;
   OnMouseEvent(e);
}

// Note that OnKeyDown has to handle navigation because wxWANTS_CHARS
// flag was set - see above.
void AButton::OnKeyDown(wxKeyEvent & event)
{
   switch( event.GetKeyCode() )
   {
   case WXK_RIGHT:
      Navigate(wxNavigationKeyEvent::IsForward);
      break;
   case WXK_LEFT:
      Navigate(wxNavigationKeyEvent::IsBackward);
      break;
   case WXK_TAB:
      Navigate(event.ShiftDown()
               ? wxNavigationKeyEvent::IsBackward
               : wxNavigationKeyEvent::IsForward);
      break;
   case WXK_RETURN:
   case WXK_NUMPAD_ENTER:
      if( !mEnabled )
         break;
      mWasShiftDown = event.ShiftDown();
      mWasControlDown = event.ControlDown();
      Click();
      break;
   default:
      event.Skip();
   }
}

void AButton::OnSetFocus(wxFocusEvent & WXUNUSED(event))
{
   Refresh( false );
}

void AButton::OnKillFocus(wxFocusEvent & WXUNUSED(event))
{
   Refresh( false );
}

bool AButton::WasShiftDown()
{
   return mWasShiftDown;
}

bool AButton::WasControlDown()
{
   return mWasControlDown;
}

void AButton::Enable()
{
   wxWindow::Enable(true);
   mEnabled = true;
   Refresh(false);
}

void AButton::Disable()
{
   // Bug 1565: Tooltips not showing on disabled buttons.
   // The fix is to NOT tell windows that the button is disabled.
   // The button's appearance will still change to show it is disabled
   // since we control that rather than windows.
#ifndef __WXMSW__
   wxWindow::Enable(false);
#endif
   mEnabled = false;
   if (GetCapture()==this)
      ReleaseMouse();
   Refresh(false);
}

void AButton::PushDown()
{
   mButtonIsDown = true;
   this->Refresh(false);
}

void AButton::PopUp()
{
   mButtonIsDown = false;

   if (GetCapture()==this)
      ReleaseMouse();

   this->Refresh(false);
}

void AButton::Click()
{
   wxCommandEvent event(wxEVT_COMMAND_BUTTON_CLICKED, GetId());
   event.SetEventObject(this);
   // Be sure to use SafelyProcessEvent so that exceptions do not propagate
   // out of DoDefaultAction
   GetEventHandler()->SafelyProcessEvent(event);
}

void AButton::SetShift(bool shift)
{
   mWasShiftDown = shift;
}

void AButton::SetControl(bool control)
{
   mWasControlDown = control;
}

auto AButton::TemporarilyAllowFocus() -> TempAllowFocus {
   s_AcceptsFocus = true;
   return TempAllowFocus{ &s_AcceptsFocus };
}

#if wxUSE_ACCESSIBILITY

AButtonAx::AButtonAx( wxWindow *window ):
   wxWindowAccessible( window )
{
}

AButtonAx::~AButtonAx()
{
}

// Performs the default action. childId is 0 (the action for this object)
// or > 0 (the action for a child).
// Return wxACC_NOT_SUPPORTED if there is no default action for this
// window (e.g. an edit control).
wxAccStatus AButtonAx::DoDefaultAction(int WXUNUSED(childId))
{
   AButton *ab = wxDynamicCast( GetWindow(), AButton );

   if(ab && ab->IsEnabled()) {
      ab->mWasShiftDown = false;
      ab->mWasControlDown = false;
      ab->Click();
   }

   return wxACC_OK;
}

// Retrieves the address of an IDispatch interface for the specified child.
// All objects must support this property.
wxAccStatus AButtonAx::GetChild( int childId, wxAccessible** child )
{
   if( childId == wxACC_SELF )
   {
      *child = this;
   }
   else
   {
      *child = NULL;
   }

   return wxACC_OK;
}

// Gets the number of children.
wxAccStatus AButtonAx::GetChildCount(int* childCount)
{
   *childCount = 0;

   return wxACC_OK;
}

// Gets the default action for this object (0) or > 0 (the action for
// a child).  Return wxACC_OK even if there is no action. actionName
// is the action, or the empty string if there is no action.  The
// retrieved string describes the action that is performed on an
// object, not what the object does as a result. For example, a
// toolbar button that prints a document has a default action of
// "Press" rather than "Prints the current document."
wxAccStatus AButtonAx::GetDefaultAction(int WXUNUSED(childId), wxString* actionName)
{
   *actionName = _( "Press" );

   return wxACC_OK;
}

// Returns the description for this object or a child.
wxAccStatus AButtonAx::GetDescription( int WXUNUSED(childId), wxString *description )
{
   description->Clear();

   return wxACC_OK;
}

// Gets the window with the keyboard focus.
// If childId is 0 and child is NULL, no object in
// this subhierarchy has the focus.
// If this object has the focus, child should be 'this'.
wxAccStatus AButtonAx::GetFocus(int* childId, wxAccessible** child)
{
   *childId = 0;
   *child = this;

   return wxACC_OK;
}

// Returns help text for this object or a child, similar to tooltip text.
wxAccStatus AButtonAx::GetHelpText( int WXUNUSED(childId), wxString *helpText )
{
#if wxUSE_TOOLTIPS // Not available in wxX11
   AButton *ab = wxDynamicCast( GetWindow(), AButton );

   wxToolTip *pTip = ab->GetToolTip();
   if( pTip )
   {
      *helpText = pTip->GetTip();
   }

   return wxACC_OK;
#else
   helpText->Clear();

   return wxACC_NOT_SUPPORTED;
#endif
}

// Returns the keyboard shortcut for this object or child.
// Return e.g. ALT+K
wxAccStatus AButtonAx::GetKeyboardShortcut( int WXUNUSED(childId), wxString *shortcut )
{
   shortcut->Clear();

   return wxACC_OK;
}

// Returns the rectangle for this object (id = 0) or a child element (id > 0).
// rect is in screen coordinates.
wxAccStatus AButtonAx::GetLocation( wxRect& rect, int WXUNUSED(elementId) )
{
   AButton *ab = wxDynamicCast( GetWindow(), AButton );

   rect = ab->GetRect();
   rect.SetPosition( ab->GetParent()->ClientToScreen( rect.GetPosition() ) );

   return wxACC_OK;
}

// Gets the name of the specified object.
wxAccStatus AButtonAx::GetName(int WXUNUSED(childId), wxString* name)
{
   AButton *ab = wxDynamicCast( GetWindow(), AButton );

   *name = ab->GetName();
   if( name->IsEmpty() )
   {
      *name = ab->GetLabel();
   }

   if( name->IsEmpty() )
   {
      *name = _("Button");
   }

   return wxACC_OK;
}

// Returns a role constant.
wxAccStatus AButtonAx::GetRole(int WXUNUSED(childId), wxAccRole* role)
{
   *role = wxROLE_SYSTEM_PUSHBUTTON;

   return wxACC_OK;
}

// Gets a variant representing the selected children
// of this object.
// Acceptable values:
// - a null variant (IsNull() returns TRUE)
// - a list variant (GetType() == wxT("list"))
// - an integer representing the selected child element,
//   or 0 if this object is selected (GetType() == wxT("long"))
// - a "void*" pointer to a wxAccessible child object
wxAccStatus AButtonAx::GetSelections( wxVariant * WXUNUSED(selections) )
{
   return wxACC_NOT_IMPLEMENTED;
}

// Returns a state constant.
wxAccStatus AButtonAx::GetState(int WXUNUSED(childId), long* state)
{
   AButton *ab = wxDynamicCast( GetWindow(), AButton );

   switch( ab->GetState() )
   {
      case AButton::AButtonDown:
         *state = wxACC_STATE_SYSTEM_PRESSED | wxACC_STATE_SYSTEM_FOCUSABLE;
      break;

      case AButton::AButtonOver:
         *state = wxACC_STATE_SYSTEM_HOTTRACKED | wxACC_STATE_SYSTEM_FOCUSABLE;
      break;

      case AButton::AButtonDis:
         *state = wxACC_STATE_SYSTEM_UNAVAILABLE;
      break;

      default:
         *state = wxACC_STATE_SYSTEM_FOCUSABLE;
      break;
   }

   *state |= ( ab == wxWindow::FindFocus() ? wxACC_STATE_SYSTEM_FOCUSED : 0 );

   return wxACC_OK;
}

// Returns a localized string representing the value for the object
// or child.
wxAccStatus AButtonAx::GetValue(int WXUNUSED(childId), wxString* WXUNUSED(strValue))
{
   return wxACC_NOT_SUPPORTED;
}

#endif
