/////////////////////////////////////////////////////////////////////////////
// Name:        src/msw/control.cpp
// Purpose:     wxControl class
// Author:      Julian Smart
// Created:     01/02/97
// Copyright:   (c) Julian Smart
// Licence:     wxWindows licence
/////////////////////////////////////////////////////////////////////////////

// ============================================================================
// declarations
// ============================================================================

// ----------------------------------------------------------------------------
// headers
// ----------------------------------------------------------------------------

// For compilers that support precompilation, includes "wx.h".
#include "wx/wxprec.h"


#if wxUSE_CONTROLS

#include "wx/control.h"

#ifndef WX_PRECOMP
    #include "wx/msw/wrapcctl.h" // include <commctrl.h> "properly"
    #include "wx/event.h"
    #include "wx/app.h"
    #include "wx/dcclient.h"
    #include "wx/log.h"
    #include "wx/settings.h"
    #include "wx/ctrlsub.h"
    #include "wx/msw/private.h"
    #include "wx/msw/missing.h"
#endif

#include "wx/renderer.h"
#include "wx/msw/uxtheme.h"
#include "wx/msw/dc.h"          // for wxDCTemp
#include "wx/msw/ownerdrawnbutton.h"
#include "wx/msw/private/darkmode.h"
#include "wx/msw/private/winstyle.h"

// ----------------------------------------------------------------------------
// wxWin macros
// ----------------------------------------------------------------------------

wxIMPLEMENT_ABSTRACT_CLASS(wxControl, wxWindow);

// ============================================================================
// wxControl implementation
// ============================================================================

// ----------------------------------------------------------------------------
// control window creation
// ----------------------------------------------------------------------------

bool wxControl::Create(wxWindow *parent,
                       wxWindowID id,
                       const wxPoint& pos,
                       const wxSize& size,
                       long style,
                       const wxValidator& wxVALIDATOR_PARAM(validator),
                       const wxString& name)
{
    if ( !wxWindow::Create(parent, id, pos, size, style, name) )
        return false;

#if wxUSE_VALIDATORS
    SetValidator(validator);
#endif

    return true;
}

bool wxControl::MSWCreateControl(const wxChar *classname,
                                 const wxString& label,
                                 const wxPoint& pos,
                                 const wxSize& size)
{
    WXDWORD exstyle;
    WXDWORD msStyle = MSWGetStyle(GetWindowStyle(), &exstyle);

    return MSWCreateControl(classname, msStyle, pos, size, label, exstyle);
}

bool wxControl::MSWCreateControl(const wxChar *classname,
                                 WXDWORD style,
                                 const wxPoint& pos,
                                 const wxSize& size,
                                 const wxString& label,
                                 WXDWORD exstyle)
{
    // all controls should have this style
    style |= WS_CHILD;

    // create the control visible if it's currently shown for wxWidgets
    if ( m_isShown )
    {
        style |= WS_VISIBLE;
    }

    // choose the position for the control: we have a problem with default size
    // here as we can't calculate the best size before the control exists
    // (DoGetBestSize() may need to use m_hWnd), so just choose the minimal
    // possible but non 0 size because 0 window width/height result in problems
    // elsewhere
    int x = pos.x == wxDefaultCoord ? 0 : pos.x,
        y = pos.y == wxDefaultCoord ? 0 : pos.y,
        w = size.x == wxDefaultCoord ? 1 : size.x,
        h = size.y == wxDefaultCoord ? 1 : size.y;

    // ... and adjust it to account for a possible parent frames toolbar
    AdjustForParentClientOrigin(x, y);

    m_hWnd = MSWCreateWindowAtAnyPosition
             (
              exstyle,            // extended style
              classname,          // the kind of control to create
              label.t_str(),      // the window name
              style,              // the window style
              x, y, w, h,         // the window position and size
              GetHwndOf(GetParent()),         // parent
              GetId()             // child id
             );

    if ( !m_hWnd )
    {
        return false;
    }

    MSWDarkModeSupport support;
    if ( wxMSWDarkMode::IsActive() && MSWGetDarkModeSupport(support) )
    {
        wxMSWDarkMode::AllowForWindow(m_hWnd, support.themeName, support.themeId);

        if ( support.setForeground )
            SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOXTEXT));

        if ( const int msgTT = MSWGetToolTipMessage() )
        {
            const HWND hwndTT = (HWND)::SendMessage(GetHwnd(), msgTT, 0, 0);
            if ( ::IsWindow(hwndTT) )
                wxMSWDarkMode::AllowForWindow(hwndTT);
        }
    }

    // saving the label in m_labelOrig to return it verbatim
    // later in GetLabel()
    m_labelOrig = label;

    // install wxWidgets window proc for this window
    SubclassWin(m_hWnd);

    // set up fonts and colours
    InheritAttributes();
    if ( !m_hasFont )
    {
        if ( MSWShouldSetDefaultFont() )
        {
            SetFont(GetDefaultAttributes().font);
        }
    }

    // set the size now if no initial size specified
    SetInitialSize(size);

    if ( size.IsFullySpecified() )
    {
        // Usually all windows get WM_NCCALCSIZE when their size is set,
        // however if the initial size is fixed, it's not going to change and
        // this message won't be sent at all, meaning that we won't get a
        // chance to tell Windows that we need extra space for our custom
        // themed borders, when using them. So force sending this message by
        // using SWP_FRAMECHANGED (and use SWP_NOXXX to avoid doing anything
        // else) to fix themed borders when they're used (if they're not, this
        // is harmless, and it's simpler and more fool proof to always do it
        // rather than try to determine whether we need to do it or not).
        ::SetWindowPos(GetHwnd(), nullptr, 0, 0, 0, 0,
                       SWP_FRAMECHANGED |
                       SWP_NOSIZE |
                       SWP_NOMOVE |
                       SWP_NOZORDER |
                       SWP_NOREDRAW |
                       SWP_NOACTIVATE |
                       SWP_NOCOPYBITS |
                       SWP_NOOWNERZORDER |
                       SWP_NOSENDCHANGING);
    }

    return true;
}

bool wxControl::MSWGetDarkModeSupport(MSWDarkModeSupport& support) const
{
    // This theme works for a few controls (buttons, texts, comboboxes) and
    // doesn't seem to do any harm for those that don't support it, so use it
    // by default.
    support.themeName = L"Explorer";

    return true;
}

// ----------------------------------------------------------------------------
// various accessors
// ----------------------------------------------------------------------------

WXDWORD wxControl::MSWGetStyle(long style, WXDWORD *exstyle) const
{
    long msStyle = wxWindow::MSWGetStyle(style, exstyle);

    if ( AcceptsFocusFromKeyboard() )
    {
        msStyle |= WS_TABSTOP;
    }

    return msStyle;
}

wxSize wxControl::DoGetBestSize() const
{
    if (m_windowSizer)
       return wxControlBase::DoGetBestSize();

    return FromDIP(wxSize(DEFAULT_ITEM_WIDTH, DEFAULT_ITEM_HEIGHT));
}

wxBorder wxControl::GetDefaultBorder() const
{
    return wxControlBase::GetDefaultBorder();
}

/* static */ wxVisualAttributes
wxControl::GetClassDefaultAttributes(wxWindowVariant variant)
{
    wxVisualAttributes attrs = wxWindow::GetClassDefaultAttributes(variant);

    // most, or at least many, of the controls use the same colours as the
    // buttons -- others will have to override this (and possibly simply call
    // GetCompositeControlsDefaultAttributes() from their versions)
    attrs.colFg = wxSystemSettings::GetColour(wxSYS_COLOUR_BTNTEXT);

    return attrs;
}

// ----------------------------------------------------------------------------
// message handling
// ----------------------------------------------------------------------------

bool wxControl::ProcessCommand(wxCommandEvent& event)
{
    return HandleWindowEvent(event);
}

bool wxControl::MSWOnNotify(int idCtrl,
                            WXLPARAM lParam,
                            WXLPARAM* result)
{
    wxEventType eventType wxDUMMY_INITIALIZE(wxEVT_NULL);

    NMHDR *hdr = (NMHDR*) lParam;
    switch ( hdr->code )
    {
        case NM_CLICK:
            eventType = wxEVT_COMMAND_LEFT_CLICK;
            break;

        case NM_DBLCLK:
            eventType = wxEVT_COMMAND_LEFT_DCLICK;
            break;

        case NM_RCLICK:
            eventType = wxEVT_COMMAND_RIGHT_CLICK;
            break;

        case NM_RDBLCLK:
            eventType = wxEVT_COMMAND_RIGHT_DCLICK;
            break;

        case NM_SETFOCUS:
            eventType = wxEVT_COMMAND_SET_FOCUS;
            break;

        case NM_KILLFOCUS:
            eventType = wxEVT_COMMAND_KILL_FOCUS;
            break;

        case NM_RETURN:
            eventType = wxEVT_COMMAND_ENTER;
            break;

        default:
            return wxWindow::MSWOnNotify(idCtrl, lParam, result);
    }

    wxCommandEvent event(wxEVT_NULL, m_windowId);
    event.SetEventType(eventType);
    event.SetEventObject(this);

    return HandleWindowEvent(event);
}

WXHBRUSH wxControl::DoMSWControlColor(WXHDC pDC, wxColour colBg, WXHWND hWnd)
{
    HDC hdc = (HDC)pDC;

    wxColour colFg;

    WXHBRUSH hbr = 0;
    if ( !colBg.IsOk() )
    {
        wxWindow *win = wxFindWinFromHandle( hWnd );
        if ( !win )
        {
            // If this HWND doesn't correspond to a wxWindow, it still might be
            // one of its children for which we need to set the background
            // brush, e.g. this is the case for the EDIT control that is part
            // of wxComboBox but also e.g. of wxSlider label HWNDs which are
            // logically part of it, but are siblings of the main control at
            // Windows level.
            //
            // So check whether it's a sibling of this window which is part of
            // the same wx object.
            if ( ContainsHWND(hWnd) )
            {
                win = this;
            }
            else // Or maybe a child sub-window of this one.
            {
                HWND parent = ::GetParent(hWnd);
                if ( parent )
                {
                    wxWindow *winParent = wxFindWinFromHandle( parent );
                    if( winParent && winParent->ContainsHWND( hWnd ) )
                        win = winParent;
                 }
            }
        }

        if ( win )
            hbr = win->MSWGetBgBrush(pDC);

        if ( !hbr )
        {
            // We always need to use custom background in dark mode. And in
            // light mode, we have to use it if the control uses a non-default
            // foreground too because if we didn't, this function would return
            // 0 and everything done by it would be ignored -- so ensure we use
            // a valid value in both of these cases.
            if ( wxMSWDarkMode::IsActive() || m_hasFgCol )
                colBg = GetBackgroundColour();
        }
    }

    // use the background colour override if a valid colour is given: this is
    // used when the control is disabled to grey it out and also if colBg was
    // set just above
    if ( colBg.IsOk() )
    {
        wxBrush *brush = wxTheBrushList->FindOrCreateBrush(colBg);
        hbr = (WXHBRUSH)brush->GetResourceHandle();
    }

    // always set the foreground colour if we changed the background, whether
    // m_hasFgCol is true or not: if it true, we must do it, of course, but
    // even if it isn't, we must set the default foreground explicitly as by
    // default just the simple black is used
    if ( hbr )
    {
        if ( !colFg.IsOk() )
            colFg = GetForegroundColour();
        ::SetTextColor(hdc, wxColourToRGB(colFg));
    }

    // finally also set the background colour for text drawing: without this,
    // the text in an edit control is drawn using the default background even
    // if we return a valid brush
    if ( colBg.IsOk() || m_hasBgCol )
    {
        if ( !colBg.IsOk() )
            colBg = GetBackgroundColour();

        ::SetBkColor(hdc, wxColourToRGB(colBg));
    }

    return hbr;
}

WXHBRUSH wxControl::MSWControlColor(WXHDC pDC, WXHWND hWnd)
{
    if ( HasTransparentBackground() )
        ::SetBkMode((HDC)pDC, TRANSPARENT);

    // don't pass any background colour to DoMSWControlColor(), our own
    // background colour will be used by it only if it is set, otherwise the
    // defaults will be used
    return DoMSWControlColor(pDC, wxColour(), hWnd);
}

WXHBRUSH wxControl::MSWControlColorDisabled(WXHDC pDC)
{
    return DoMSWControlColor(pDC,
                             wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE),
                             GetHWND());
}

wxWindow* wxControl::MSWFindItem(long id, WXHWND hWnd) const
{
    // is it us or one of our "internal" children?
    if ( id == GetId() || (GetSubcontrols().Index(id) != wxNOT_FOUND) )
        return const_cast<wxControl *>(this);

    return wxControlBase::MSWFindItem(id, hWnd);
}

// ----------------------------------------------------------------------------
// Owner drawn buttons support.
// ----------------------------------------------------------------------------

void
wxMSWOwnerDrawnButtonBase::MSWMakeOwnerDrawnIfNecessary(const wxColour& colFg)
{
    // The only way to change the checkbox foreground colour when using
    // themes is to owner draw it.
    if ( wxUxThemeIsActive() )
        MSWMakeOwnerDrawn(colFg.IsOk());
}

bool wxMSWOwnerDrawnButtonBase::MSWIsOwnerDrawn() const
{
    return
        (::GetWindowLong(GetHwndOf(m_win), GWL_STYLE) & BS_OWNERDRAW) == BS_OWNERDRAW;
}

void wxMSWOwnerDrawnButtonBase::MSWMakeOwnerDrawn(bool ownerDrawn)
{
    wxMSWWinStyleUpdater updateStyle(GetHwndOf(m_win));

    // note that BS_CHECKBOX & BS_OWNERDRAW != 0 so we can't operate on
    // them as on independent style bits
    if ( ownerDrawn )
    {
        updateStyle.TurnOff(BS_TYPEMASK).TurnOn(BS_OWNERDRAW);

        m_win->Bind(wxEVT_ENTER_WINDOW,
                    &wxMSWOwnerDrawnButtonBase::OnMouseEnterOrLeave, this);
        m_win->Bind(wxEVT_LEAVE_WINDOW,
                    &wxMSWOwnerDrawnButtonBase::OnMouseEnterOrLeave, this);

        m_win->Bind(wxEVT_LEFT_DOWN,
                    &wxMSWOwnerDrawnButtonBase::OnMouseLeft, this);
        m_win->Bind(wxEVT_LEFT_UP,
                    &wxMSWOwnerDrawnButtonBase::OnMouseLeft, this);

        m_win->Bind(wxEVT_SET_FOCUS,
                    &wxMSWOwnerDrawnButtonBase::OnFocus, this);

        m_win->Bind(wxEVT_KILL_FOCUS,
                    &wxMSWOwnerDrawnButtonBase::OnFocus, this);
    }
    else // reset to default colour
    {
        updateStyle.TurnOff(BS_OWNERDRAW).TurnOn(MSWGetButtonStyle());

        m_win->Unbind(wxEVT_ENTER_WINDOW,
                      &wxMSWOwnerDrawnButtonBase::OnMouseEnterOrLeave, this);
        m_win->Unbind(wxEVT_LEAVE_WINDOW,
                      &wxMSWOwnerDrawnButtonBase::OnMouseEnterOrLeave, this);

        m_win->Unbind(wxEVT_LEFT_DOWN,
                      &wxMSWOwnerDrawnButtonBase::OnMouseLeft, this);
        m_win->Unbind(wxEVT_LEFT_UP,
                      &wxMSWOwnerDrawnButtonBase::OnMouseLeft, this);

        m_win->Unbind(wxEVT_SET_FOCUS,
                      &wxMSWOwnerDrawnButtonBase::OnFocus, this);
        m_win->Unbind(wxEVT_KILL_FOCUS,
                      &wxMSWOwnerDrawnButtonBase::OnFocus, this);
    }

    updateStyle.Apply();

    if ( !ownerDrawn )
        MSWOnButtonResetOwnerDrawn();
}

void wxMSWOwnerDrawnButtonBase::OnMouseEnterOrLeave(wxMouseEvent& event)
{
    if ( event.GetEventType() == wxEVT_LEAVE_WINDOW )
        m_isPressed = false;

    m_win->Refresh();

    event.Skip();
}

void wxMSWOwnerDrawnButtonBase::OnMouseLeft(wxMouseEvent& event)
{
    // TODO: we should capture the mouse here to be notified about left up
    //       event but this interferes with BN_CLICKED generation so if we
    //       want to do this we'd need to generate them ourselves
    m_isPressed = event.GetEventType() == wxEVT_LEFT_DOWN;
    m_win->Refresh();

    event.Skip();
}

void wxMSWOwnerDrawnButtonBase::OnFocus(wxFocusEvent& event)
{
    m_win->Refresh();

    event.Skip();
}

bool wxMSWOwnerDrawnButtonBase::MSWDrawButton(WXDRAWITEMSTRUCT *item)
{
    DRAWITEMSTRUCT *dis = (DRAWITEMSTRUCT *)item;

    if ( !MSWIsOwnerDrawn() || dis->CtlType != ODT_BUTTON )
        return false;

    // shall we draw a focus rect?
    const bool isFocused = m_isPressed || m_win->HasFocus();

    int flags = MSWGetButtonCheckedFlag();

    if ( dis->itemState & ODS_SELECTED )
        flags |= wxCONTROL_SELECTED | wxCONTROL_PRESSED;

    if ( !m_win->IsEnabled() )
        flags |= wxCONTROL_DISABLED;

    if ( m_isPressed )
        flags |= wxCONTROL_PRESSED;

    if ( wxFindWindowAtPoint(wxGetMousePosition()) == m_win )
        flags |= wxCONTROL_CURRENT;


    // calculate the rectangles for the button itself and the label
    HDC hdc = dis->hDC;
    const RECT& rect = dis->rcItem;

    // calculate the rectangles for the button itself and the label
    const wxSize bestSize = m_win->GetBestSize();
    RECT rectButton,
         rectLabel;
    rectLabel.top = rect.top + (rect.bottom - rect.top - bestSize.y) / 2;
    rectLabel.bottom = rectLabel.top + bestSize.y;

    // choose the values consistent with those used for native, non
    // owner-drawn, buttons

    const int spacing = m_win->FromDIP(3);
    const wxSize cbSize = wxRendererNative::Get().GetCheckBoxSize(m_win, flags);

    const int buttonSize = wxMin(cbSize.y, m_win->GetSize().y);
    rectButton.top = rect.top + (rect.bottom - rect.top - buttonSize) / 2;
    rectButton.bottom = rectButton.top + buttonSize;

    const bool isRightAligned = m_win->HasFlag(wxALIGN_RIGHT);
    if ( isRightAligned )
    {
        rectButton.right = rect.right;
        rectButton.left = rectButton.right - buttonSize;

        rectLabel.right = rectButton.left - spacing;
        rectLabel.left = rect.left;
    }
    else // normal, left-aligned button
    {
        rectButton.left = rect.left;
        rectButton.right = rectButton.left + buttonSize;

        rectLabel.left = spacing + rectButton.right;
        rectLabel.right = rect.right;
    }

    // Erase the background.
    HBRUSH hbr = m_win->MSWGetBgBrush(hdc);
    if ( !hbr && wxMSWDarkMode::IsActive() )
    {
        // We always need to use custom background in dark mode, default
        // behaviour is never correct.
        auto const colBg = m_win->GetBackgroundColour();
        wxBrush* brush = wxTheBrushList->FindOrCreateBrush(colBg);
        hbr = (WXHBRUSH)brush->GetResourceHandle();
    }

    ::FillRect(hdc, &rect, hbr);

    // draw the button itself
    wxDCTemp dc(hdc);

    MSWDrawButtonBitmap(dc, wxRectFromRECT(rectButton), flags);

    // draw the text
    const wxString& label = m_win->GetLabel();

    // first we need to measure it
    UINT fmt = DT_NOCLIP;

    // drawing underlying doesn't look well with focus rect (and the native
    // control doesn't do it)
    if ( isFocused )
        fmt |= DT_HIDEPREFIX;
    if ( isRightAligned )
        fmt |= DT_RIGHT;
    // TODO: also use DT_HIDEPREFIX if the system is configured so

    // we need to get the label real size first if we have to draw a focus rect
    // around it
    if ( isFocused )
    {
        RECT oldLabelRect = rectLabel; // needed if right aligned

        if ( !::DrawText(hdc, label.t_str(), label.length(), &rectLabel,
                         fmt | DT_CALCRECT) )
        {
            wxLogLastError(wxT("DrawText(DT_CALCRECT)"));
        }

        if ( isRightAligned )
        {
            // move the label rect to the right
            const int labelWidth = rectLabel.right - rectLabel.left;
            rectLabel.right = oldLabelRect.right;
            rectLabel.left = rectLabel.right - labelWidth;
        }
    }

    if ( flags & wxCONTROL_DISABLED )
    {
        ::SetTextColor(hdc, ::GetSysColor(COLOR_GRAYTEXT));
    }

    if ( !::DrawText(hdc, label.t_str(), label.length(), &rectLabel, fmt) )
    {
        wxLogLastError(wxT("DrawText()"));
    }

    // finally draw the focus
    if ( isFocused )
    {
        rectLabel.left--;
        rectLabel.right++;
        if ( !::DrawFocusRect(hdc, &rectLabel) )
        {
            wxLogLastError(wxT("DrawFocusRect()"));
        }
    }

    return true;
}

// ----------------------------------------------------------------------------
// wxControlWithItems
// ----------------------------------------------------------------------------

void wxControlWithItems::MSWAllocStorage(const wxArrayStringsAdapter& items,
                                         unsigned wm)
{
    const unsigned numItems = items.GetCount();
    unsigned long totalTextLength = numItems; // for trailing '\0' characters
    for ( unsigned i = 0; i < numItems; ++i )
    {
        totalTextLength += items[i].length();
    }

    if ( SendMessage((HWND)MSWGetItemsHWND(), wm, numItems,
                     (LPARAM)totalTextLength*sizeof(wxChar)) == LB_ERRSPACE )
    {
        wxLogLastError(wxT("SendMessage(XX_INITSTORAGE)"));
    }
}

int wxControlWithItems::MSWInsertOrAppendItem(unsigned pos,
                                              const wxString& item,
                                              unsigned wm)
{
    LRESULT n = SendMessage((HWND)MSWGetItemsHWND(), wm, pos,
                            wxMSW_CONV_LPARAM(item));
    if ( n == CB_ERR || n == CB_ERRSPACE )
    {
        wxLogLastError(wxT("SendMessage(XX_ADD/INSERTSTRING)"));
        return wxNOT_FOUND;
    }

    return n;
}

// ---------------------------------------------------------------------------
// global functions
// ---------------------------------------------------------------------------

// this is used in radiobox.cpp and slider95.cpp and should be removed as soon
// as it is not needed there any more!
//
// Call this repeatedly for several wnds to find the overall size
// of the widget.
// Call it initially with wxDefaultCoord for all values in rect.
// Keep calling for other widgets, and rect will be modified
// to calculate largest bounding rectangle.
void wxFindMaxSize(WXHWND wnd, RECT *rect)
{
    int left = rect->left;
    int right = rect->right;
    int top = rect->top;
    int bottom = rect->bottom;

    GetWindowRect((HWND) wnd, rect);

    if (left < 0)
        return;

    if (left < rect->left)
        rect->left = left;

    if (right > rect->right)
        rect->right = right;

    if (top < rect->top)
        rect->top = top;

    if (bottom > rect->bottom)
        rect->bottom = bottom;
}

#endif // wxUSE_CONTROLS
