///////////////////////////////////////////////////////////////////////////////
// Name:        src/generic/tipwin.cpp
// Purpose:     implementation of wxTipWindow
// Author:      Vadim Zeitlin
// Created:     10.09.00
// Copyright:   (c) 2000 Vadim Zeitlin <zeitlin@dptmaths.ens-cachan.fr>
// Licence:     wxWindows licence
///////////////////////////////////////////////////////////////////////////////

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

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

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


#if wxUSE_TIPWINDOW

#include "wx/tipwin.h"

#ifndef WX_PRECOMP
    #include "wx/dcclient.h"
    #include "wx/timer.h"
    #include "wx/settings.h"
#endif // WX_PRECOMP

#include "wx/display.h"
#include "wx/vector.h"

// ----------------------------------------------------------------------------
// constants
// ----------------------------------------------------------------------------

static const wxCoord TEXT_MARGIN_X = 3;
static const wxCoord TEXT_MARGIN_Y = 3;

// ----------------------------------------------------------------------------
// wxTipWindowView
// ----------------------------------------------------------------------------

// Viewer window to put in the frame
class WXDLLEXPORT wxTipWindowView : public wxWindow
{
public:
    wxTipWindowView(wxWindow *parent);

    // event handlers
    void OnPaint(wxPaintEvent& event);
    void OnMouseClick(wxMouseEvent& event);
    void OnMouseMove(wxMouseEvent& event);

    // calculate the client rect we need to display the text
    void Adjust(const wxString& text, wxCoord maxLength);

private:
    wxTipWindow* m_parent;

    wxVector<wxString> m_textLines;
    wxCoord m_heightLine;


    wxDECLARE_EVENT_TABLE();
    wxDECLARE_NO_COPY_CLASS(wxTipWindowView);
};

// ============================================================================
// implementation
// ============================================================================

// ----------------------------------------------------------------------------
// event tables
// ----------------------------------------------------------------------------

wxBEGIN_EVENT_TABLE(wxTipWindow, wxPopupTransientWindow)
    EVT_LEFT_DOWN(wxTipWindow::OnMouseClick)
    EVT_RIGHT_DOWN(wxTipWindow::OnMouseClick)
    EVT_MIDDLE_DOWN(wxTipWindow::OnMouseClick)
wxEND_EVENT_TABLE()

wxBEGIN_EVENT_TABLE(wxTipWindowView, wxWindow)
    EVT_PAINT(wxTipWindowView::OnPaint)

    EVT_LEFT_DOWN(wxTipWindowView::OnMouseClick)
    EVT_RIGHT_DOWN(wxTipWindowView::OnMouseClick)
    EVT_MIDDLE_DOWN(wxTipWindowView::OnMouseClick)

    EVT_MOTION(wxTipWindowView::OnMouseMove)
wxEND_EVENT_TABLE()

// ----------------------------------------------------------------------------
// wxTipWindow
// ----------------------------------------------------------------------------

wxTipWindow::Ref::~Ref()
{
    if (m_ptr)
    {
        m_ptr->SetTipWindowPtr(nullptr);
    }
}

wxTipWindow::Ref::Ref(Ref&& other)
{
    m_ptr = other.m_ptr;
    if (m_ptr)
    {
        other.m_ptr = nullptr;
        m_ptr->SetTipWindowPtr(&m_ptr);
    }
}

wxTipWindow::Ref& wxTipWindow::Ref::operator=(Ref&& other)
{
    if (m_ptr != other.m_ptr)
    {
        if (m_ptr)
        {
            m_ptr->SetTipWindowPtr(nullptr);
        }

        m_ptr = other.m_ptr;
        if (m_ptr)
        {
            other.m_ptr = nullptr;
            m_ptr->SetTipWindowPtr(&m_ptr);
        }
    }

    return *this;
}

wxTipWindow::Ref& wxTipWindow::Ref::operator=(std::nullptr_t)
{
    if (m_ptr)
    {
        m_ptr->SetTipWindowPtr(nullptr);
        m_ptr = nullptr;
    }

    return *this;
}

wxTipWindow::Ref wxTipWindow::New(wxWindow *parent,
            const wxString& text,
            wxCoord maxLength /*= 100*/,
            wxRect *rectBound /*= nullptr*/)
{
    std::unique_ptr<wxTipWindow> temp(new wxTipWindow);
    wxTipWindow::Ref retval;
    retval.m_ptr = temp.get();

    if (!temp->Create(parent, text, maxLength, &retval.m_ptr, rectBound))
    {
        return Ref();
    }

    temp.release();
    return retval;
}

wxTipWindow::wxTipWindow()
{
    SetTipWindowPtr(nullptr);
    m_view = nullptr;
}

bool wxTipWindow::Create(wxWindow *parent,
                         const wxString& text,
                         wxCoord maxLength,
                         wxTipWindow** windowPtr,
                         wxRect *rectBounds)
{
    if (!wxPopupTransientWindow::Create(parent))
    {
        return false;
    }

    SetTipWindowPtr(windowPtr);
    if ( rectBounds )
    {
        SetBoundingRect(*rectBounds);
    }

    // set colours
    SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_INFOTEXT));
    SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_INFOBK));

    int x, y;
    wxGetMousePosition(&x, &y);

    // move to the center of the target display so wxTipWindowView will use the
    // correct DPI
    wxPoint posScreen;
    wxSize sizeScreen;

    const int displayNum = wxDisplay::GetFromPoint(wxPoint(x, y));
    if ( displayNum != wxNOT_FOUND )
    {
        const wxRect rectScreen = wxDisplay(displayNum).GetGeometry();
        posScreen = rectScreen.GetPosition();
        sizeScreen = rectScreen.GetSize();
    }
    else // outside of any display?
    {
        // just use the primary one then
        posScreen = wxPoint(0, 0);
        sizeScreen = wxGetDisplaySize();
    }
    wxPoint center(posScreen.x + sizeScreen.GetWidth() / 2,
                   posScreen.y + sizeScreen.GetHeight() / 2);
    Move(center, wxSIZE_NO_ADJUSTMENTS);

    // set size, position and show it
    m_view = new wxTipWindowView(this);
    m_view->Adjust(text, FromDIP(parent->ToDIP(maxLength)) );

    // we want to show the tip below the mouse, not over it, make sure to not
    // overflow into the next display
    //
    // NB: the reason we use "/ 2" here is that we don't know where the current
    //     cursors hot spot is... it would be nice if we could find this out
    //     though
    int cursorOffset = wxSystemSettings::GetMetric(wxSYS_CURSOR_Y, this) / 2;
    if (y + cursorOffset >= posScreen.y + sizeScreen.GetHeight())
        cursorOffset = posScreen.y + sizeScreen.GetHeight() - y - 1;
    y += cursorOffset;

    Position(wxPoint(x, y), wxSize(0,0));
    Popup(m_view);
    #ifdef __WXGTK__
        m_view->CaptureMouse();
    #endif

    return true;
}

wxTipWindow::~wxTipWindow()
{
    if ( m_windowPtr )
    {
        *m_windowPtr = nullptr;
    }
    #ifdef __WXGTK__
        if ( m_view && m_view->HasCapture() )
            m_view->ReleaseMouse();
    #endif
}

void wxTipWindow::OnMouseClick(wxMouseEvent& WXUNUSED(event))
{
    Close();
}

void wxTipWindow::OnDismiss()
{
    Close();
}

void wxTipWindow::SetBoundingRect(const wxRect& rectBound)
{
    m_rectBound = rectBound;
}

void wxTipWindow::Close()
{
    if ( m_windowPtr )
    {
        *m_windowPtr = nullptr;
        m_windowPtr = nullptr;
    }

    Show(false);
    #ifdef __WXGTK__
        if ( m_view->HasCapture() )
            m_view->ReleaseMouse();
    #endif
    // Under OS X and Qt we get destroyed because of wxEVT_KILL_FOCUS generated by
    // Show(false).
    #if !defined(__WXOSX__) && !defined(__WXQT__)
        Destroy();
    #endif
}

// ----------------------------------------------------------------------------
// wxTipWindowView
// ----------------------------------------------------------------------------

wxTipWindowView::wxTipWindowView(wxWindow *parent)
               : wxWindow(parent, wxID_ANY,
                          wxDefaultPosition, wxDefaultSize,
                          wxNO_BORDER)
{
    // set colours
    SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_INFOTEXT));
    SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_INFOBK));

    m_parent = (wxTipWindow*)parent;

    m_heightLine = 0;
}

void wxTipWindowView::Adjust(const wxString& text, wxCoord maxLength)
{
    wxInfoDC dc(this);

    // calculate the length: we want each line be no longer than maxLength
    // pixels and we only break lines at words boundary
    wxString current;
    wxCoord height, width,
            widthMax = 0;

    bool breakLine = false;
    for ( const wxChar *p = text.c_str(); ; p++ )
    {
        if ( *p == wxT('\n') || *p == wxT('\0') )
        {
            dc.GetTextExtent(current, &width, &height);
            if ( width > widthMax )
                widthMax = width;

            if ( height > m_heightLine )
                m_heightLine = height;

            m_textLines.push_back(current);

            if ( !*p )
            {
                // end of text
                break;
            }

            current.clear();
            breakLine = false;
        }
        else if ( breakLine && (*p == wxT(' ') || *p == wxT('\t')) )
        {
            // word boundary - break the line here
            m_textLines.push_back(current);
            current.clear();
            breakLine = false;
        }
        else // line goes on
        {
            current += *p;
            dc.GetTextExtent(current, &width, &height);
            if ( width > maxLength )
                breakLine = true;

            if ( width > widthMax )
                widthMax = width;

            if ( height > m_heightLine )
                m_heightLine = height;
        }
    }

    // take into account the border size and the margins
    width  = 2*(TEXT_MARGIN_X + 1) + widthMax;
    height = 2*(TEXT_MARGIN_Y + 1) + wx_truncate_cast(wxCoord, m_textLines.size())*m_heightLine;
    m_parent->SetClientSize(width, height);
    SetSize(0, 0, width, height);
}

void wxTipWindowView::OnPaint(wxPaintEvent& WXUNUSED(event))
{
    wxPaintDC dc(this);

    wxRect rect;
    wxSize size = GetClientSize();
    rect.width = size.x;
    rect.height = size.y;

    // first filll the background
    dc.SetBrush(wxBrush(GetBackgroundColour(), wxBRUSHSTYLE_SOLID));
    dc.SetPen(wxPen(GetForegroundColour(), 1, wxPENSTYLE_SOLID));
    dc.DrawRectangle(rect);

    // and then draw the text line by line
    wxPoint pt;
    pt.x = TEXT_MARGIN_X;
    pt.y = TEXT_MARGIN_Y;
    const size_t count = m_textLines.size();
    for ( size_t n = 0; n < count; n++ )
    {
        dc.DrawText(m_textLines[n], pt);

        pt.y += m_heightLine;
    }
}

void wxTipWindowView::OnMouseClick(wxMouseEvent& WXUNUSED(event))
{
    m_parent->Close();
}

void wxTipWindowView::OnMouseMove(wxMouseEvent& event)
{
    const wxRect& rectBound = m_parent->m_rectBound;

    if ( rectBound.width &&
            !rectBound.Contains(ClientToScreen(event.GetPosition())) )
    {
        // mouse left the bounding rect, disappear
        m_parent->Close();
    }
    else
    {
        event.Skip();
    }
}

#endif // wxUSE_TIPWINDOW
