/*
 * This file is part of the Code::Blocks IDE and licensed under the GNU Lesser General Public License, version 3
 * http://www.gnu.org/licenses/lgpl-3.0.html
 *
 * $Revision: 12812 $
 * $Id: cbstyledtextctrl.cpp 12812 2022-05-14 20:49:58Z pecanh $
 * $HeadURL: https://svn.code.sf.net/p/codeblocks/code/trunk/src/sdk/cbstyledtextctrl.cpp $
 */

#include "sdk_precomp.h"
#include "cbstyledtextctrl.h"
#ifndef CB_PRECOMP
    #include <wx/gdicmn.h> // for wxPoint
    #include <wx/string.h>
    #include <wx/timer.h>

    #include "editorbase.h" // DisplayContextMenu
    #include "editormanager.h"
    #include "pluginmanager.h"
#endif

#include "cbdebugger_interfaces.h"
#include "ccmanager.h"
#include "debuggermanager.h"

static const wxString s_leftBrace(_T("([{'\""));
static const wxString s_rightBrace(_T(")]}'\""));
static const int s_indicHighlight(20);

std::map<int, std::set<int> > cbStyledTextCtrl::CharacterLexerStyles;
std::map<int, std::set<int> > cbStyledTextCtrl::StringLexerStyles;
std::map<int, std::set<int> > cbStyledTextCtrl::PreprocessorLexerStyles;
std::map<int, std::set<int> > cbStyledTextCtrl::CommentLexerStyles;

BEGIN_EVENT_TABLE(cbStyledTextCtrl, wxScintilla)
    EVT_CONTEXT_MENU(cbStyledTextCtrl::OnContextMenu)
    EVT_KILL_FOCUS  (cbStyledTextCtrl::OnKillFocus)
    EVT_MIDDLE_DOWN (cbStyledTextCtrl::OnMouseMiddleDown)
    EVT_SET_FOCUS   (cbStyledTextCtrl::OnSetFocus)
    EVT_KEY_DOWN    (cbStyledTextCtrl::OnKeyDown)
    EVT_KEY_UP      (cbStyledTextCtrl::OnKeyUp)
    EVT_LEFT_UP     (cbStyledTextCtrl::OnMouseLeftUp)
END_EVENT_TABLE()

cbStyledTextCtrl::cbStyledTextCtrl(wxWindow* pParent, int id, const wxPoint& pos, const wxSize& size, long style) :
    wxScintilla(pParent, id, pos, size, style),
    m_pParent(pParent),
    m_lastFocusTime(0L),
    m_bracePosition(wxSCI_INVALID_POSITION),
    m_lastPosition(wxSCI_INVALID_POSITION),
    m_tabSmartJump(false)
{
    //ctor
    m_braceShortcutState = false;
}

cbStyledTextCtrl::~cbStyledTextCtrl()
{
    //dtor
    // remove remaining event-handlers to avoid asserts in wx2.9
    // they might be added by plugins (at least keybinder does it), but can not
    // be removed by them, because the EditorClose-event is sent from EditorBase
    // after destroying the underlying editors (e.g. cbStyledTextCtrl).
    // Doing this here should not harm.
    while(GetEventHandler() != this)
        RemoveEventHandler(GetEventHandler());
}

// events

void cbStyledTextCtrl::OnKillAutoCompPopups() //CallAfter
{
    if ( AutoCompActive() )
        AutoCompCancel();

    if ( CallTipActive() )
        CallTipCancel();
}

void cbStyledTextCtrl::OnKillFocus(wxFocusEvent& event)
{
     event.Skip();

     // cancel auto-completion list when losing focus

    // But don't kill the popups here. When AutoComplete::Select() does not
    // find an appropriate string to select it issues a cancel to the popup
    // which issues a SetFocus() back to this editor. If we kill it here, on return
    // AutoComplete::Select() will use a popup address of 0x0; CRASH !!

    // Issue a callback to close the popups after this Kill/SetFocus finishes.
    if ( AutoCompActive() )
           CallAfter(&cbStyledTextCtrl::OnKillAutoCompPopups);

    if ( CallTipActive() )
           CallAfter(&cbStyledTextCtrl::OnKillAutoCompPopups);
 }

void cbStyledTextCtrl::OnSetFocus(wxFocusEvent& event)
{
    // store timestamp for use in cbEditor::GetControl()
    // don't use event.GetTimeStamp(), because the focus event has no timestamp !
    m_lastFocusTime = wxGetLocalTimeMillis();
    event.Skip();
}

void cbStyledTextCtrl::OnContextMenu(wxContextMenuEvent& event)
{
    if (m_pParent)
    {
        if ( EditorBase* pParent = dynamic_cast<EditorBase*>(m_pParent) )
        {
            // To prevent generating EVT_MOUSE_CAPTURE_LOST.
            if (HaveMouseCapture())
                SetMouseCapture(false);

            const bool is_right_click = event.GetPosition() != wxDefaultPosition;
            const wxPoint mp(is_right_click ? event.GetPosition() : wxDefaultPosition);
            pParent->DisplayContextMenu(mp, mtEditorManager, this);
        }
        else
            event.Skip();
    }
}

void cbStyledTextCtrl::OnMouseMiddleDown(wxMouseEvent& event)
{
    if (platform::gtk == false) // only if OnMouseMiddleDown is not already implemented by the OS
    {
        if (not Manager::Get()->GetConfigManager(_T("editor"))->ReadBool(_T("/enable_middle_mouse_paste"), false))
            {event.Skip(); return;}

        int pos = PositionFromPoint(wxPoint(event.GetX(), event.GetY()));

        if (pos == wxSCI_INVALID_POSITION)
            return;

        int start = GetSelectionStart();
        int end   = GetSelectionEnd();

        const wxString s = GetSelectedText();

        if (pos < GetCurrentPos())
        {
            start += s.length();
            end += s.length();
        }

        InsertText(pos, s);
        SetSelectionVoid(start, end);
    }
}

void cbStyledTextCtrl::OnKeyDown(wxKeyEvent& event)
{
    m_lastSelectedText = GetSelectedText();
    bool emulateDwellStart = false;

    switch ( event.GetKeyCode() )
    {
        case _T('I'):
        {
            // todo: this feature is undocumented and unexpected, remove?
            if (event.GetModifiers() == wxMOD_ALT)
                m_braceShortcutState = true;
            break;
        }

        case WXK_TAB:
        {
            if (m_tabSmartJump && event.GetModifiers() == wxMOD_NONE)
            {
                if (!AutoCompActive() && m_bracePosition != wxSCI_INVALID_POSITION)
                {
                    m_lastPosition = GetCurrentPos();
                    GotoPos(m_bracePosition);

                    // Need judge if it's the final brace
                    HighlightRightBrace();
                    if (!m_tabSmartJump && CallTipActive())
                        CallTipCancel();
                    return;
                }
            }
        }
        break;

        case WXK_BACK:
        {
            if (m_tabSmartJump)
            {
                if (!(event.ControlDown() || event.ShiftDown() || event.AltDown()))
                {
                    const int pos = GetCurrentPos();
                    const int index = s_leftBrace.Find((wxChar)GetCharAt(pos - 1));
                    if (index != wxNOT_FOUND && (wxChar)GetCharAt(pos) == s_rightBrace.GetChar(index))
                    {
                        CharRight();
                        DeleteBack();
                    }
                }
                else if (m_lastPosition != wxSCI_INVALID_POSITION && event.ControlDown())
                {
                    GotoPos(m_lastPosition);
                    m_lastPosition = wxSCI_INVALID_POSITION;
                    return;
                }
            }
        }
        break;

        case WXK_RETURN:
        case WXK_NUMPAD_ENTER:
        case WXK_ESCAPE:
        {
            if (m_tabSmartJump)
                m_tabSmartJump = false;
        }
        break;

        case WXK_CONTROL:
        {
            EmulateDwellStart();
            emulateDwellStart = true;
        }
        break;

        case WXK_UP:
        case WXK_DOWN:
        {
            if (event.GetModifiers() == wxMOD_NONE)
            {
                if (Manager::Get()->GetCCManager()->ProcessArrow(event.GetKeyCode()))
                    return;
            }
        }
        break;
        default: break;
    }

    if (event.ControlDown() && !emulateDwellStart)
        EmulateDwellStart();

    event.Skip();
}

void cbStyledTextCtrl::OnKeyUp(wxKeyEvent& event)
{
    const int keyCode = event.GetKeyCode();
    switch (keyCode)
    {
        case _T('['):   // [ {
        case _T('\''):  // ' "
#ifdef __WXMSW__
        case _T('9'):   // ( for wxMSW
#else
        case _T('('):   // ( for wxGTK
#endif
        {
            if ( !AllowTabSmartJump() )
                break;

            wxChar ch = keyCode;
            if (event.ShiftDown())
            {
                if (keyCode == _T('\''))
                    ch = _T('"');
                else if (keyCode == _T('9'))
                    ch = _T('(');
                else if (keyCode == _T('['))
                    ch = _T('{');
            }

            int index = s_leftBrace.Find(ch);
            if (index != wxNOT_FOUND && (wxChar)GetCharAt(GetCurrentPos()) == s_rightBrace.GetChar(index))
            {
                const int pos = GetCurrentPos();
                if (pos != wxSCI_INVALID_POSITION)
                {
                    m_tabSmartJump = true;
                    m_bracePosition = pos;
                }
            }
            else if (keyCode == _T('\'')) // ' "
                m_tabSmartJump = false;
        }
        break;

        case _T(']'):   // ] }
#ifdef __WXMSW__
        case _T('0'):   // ) for wxMSW
#else
        case _T(')'):   // ) for wxGTK
#endif
        {
            if (!AllowTabSmartJump())
                break;
            if (keyCode == _T('0') && !event.ShiftDown())
                break;
            m_tabSmartJump = false;
        }
        break;
        default: break;
    }

    HighlightRightBrace();
    event.Skip();
}

void cbStyledTextCtrl::OnMouseLeftUp(wxMouseEvent& event)
{
    HighlightRightBrace();
    event.Skip();
}

bool cbStyledTextCtrl::IsCharacter(int style)
{
    return CharacterLexerStyles[GetLexer()].find(style) != CharacterLexerStyles[GetLexer()].end();
}

bool cbStyledTextCtrl::IsString(int style)
{
    return StringLexerStyles[GetLexer()].find(style) != StringLexerStyles[GetLexer()].end();
}

bool cbStyledTextCtrl::IsPreprocessor(int style)
{
    return PreprocessorLexerStyles[GetLexer()].find(style) != PreprocessorLexerStyles[GetLexer()].end();
}

bool cbStyledTextCtrl::IsComment(int style)
{
    return CommentLexerStyles[GetLexer()].find(style) != CommentLexerStyles[GetLexer()].end();
}

void cbStyledTextCtrl::CallTipCancel()
{
    if (!m_tabSmartJump)
        wxScintilla::CallTipCancel();
}

bool cbStyledTextCtrl::IsBraceShortcutActive()
{
    bool state = m_braceShortcutState;
    m_braceShortcutState = false;
    return state;
}

void cbStyledTextCtrl::DoBraceCompletion(const wxChar& ch)
{
    const int pos   = GetCurrentPos();
    const int style = GetStyleAt(pos);
    if (IsComment(style) || IsComment(GetStyleAt(pos - 2)))
        return; // do nothing
    if (ch == wxT('\'') || ch == wxT('"'))
    {
        if (GetCharAt(pos) == ch)
        {
            DeleteBack();
            CharRight();
        }
        else if (!IsString(GetStyleAt(pos - 2)) && !IsCharacter(GetStyleAt(pos - 2)))
            InsertText(pos, ch);
        return; // done
    }

    if (IsString(style) || IsCharacter(style))
        return; // do nothing

    const wxString opBraces(wxT("([{")); const int opBraceIdx = opBraces.Find(ch);
    const wxString clBraces(wxT(")]}")); const int clBraceIdx = clBraces.Find(ch);
    if ( (opBraceIdx != wxNOT_FOUND) || (clBraceIdx != wxNOT_FOUND) )
    {
        if ( GetCharAt(pos) == ch )
        {
            DeleteBack();
            CharRight();
        }
        else if (opBraceIdx != wxNOT_FOUND)
        {
            int nextPos = pos;
            while ( wxIsspace(GetCharAt(nextPos)) && (nextPos < GetLength()) )
                ++nextPos;

            if (   ((wxChar)GetCharAt(nextPos) != clBraces[opBraceIdx])
                || (BraceMatch(nextPos)        != wxNOT_FOUND) )
            {
                InsertText(pos, clBraces[opBraceIdx]);
            }
        }
    }
}

bool cbStyledTextCtrl::DoSelectionBraceCompletion(const wxChar& ch)
{
    if (GetSelections() > 1)
        return false;
    if (GetLastSelectedText().IsEmpty())
        return false; // nothing changed
    const wxString braces(wxT("([{<'\")]}>'\""));
    const int braceAIdx = braces.Find(ch, true); // from end (so caret is placed after quotes)
    if (braceAIdx == wxNOT_FOUND)
        return false; // nothing changed
    const int braceBIdx = (braceAIdx + (braces.Length() / 2)) % braces.Length();
    BeginUndoAction();
    DeleteBack();
    if (braceAIdx < braceBIdx)
        InsertText(GetCurrentPos(), braces[braceAIdx] + GetLastSelectedText() + braces[braceBIdx]);
    else
        AddText(braces[braceBIdx] + GetLastSelectedText() + braces[braceAIdx]);
    EndUndoAction();
    return true; // succeeded
}

bool cbStyledTextCtrl::AllowTabSmartJump()
{
    const int pos = GetCurrentPos();
    if (pos == wxSCI_INVALID_POSITION)
        return false;

    const int style = GetStyleAt(pos);
    if (IsString(style) || IsCharacter(style) || IsComment(style) || IsPreprocessor(style))
        return !m_tabSmartJump;
    return true;
}

void cbStyledTextCtrl::HighlightRightBrace()
{
    if (m_bracePosition == wxSCI_INVALID_POSITION)
        return;

    int pos = GetCurrentPos();
    if (pos == wxSCI_INVALID_POSITION)
        return;

    const static wxColour caretForeground = GetCaretForeground();
    const static int caretWidth = GetCaretWidth();
    const int curLine = GetCurrentLine();
    const int len = GetLength();

    if (m_tabSmartJump && (curLine == LineFromPosition(m_bracePosition)))
    {
        SetIndicatorCurrent(s_indicHighlight);
        const int indPos = GetLineIndentPosition(curLine);
        IndicatorClearRange(indPos, GetLineEndPosition(curLine)-indPos);
        do
        {
            if (pos >= len)
                break;

            wxString cur((wxChar)GetCharAt(pos));
            if (cur == _T("\n"))
                break;

            int style = GetStyleAt(pos);
            if (IsComment(style))
                continue;

            if (IsString(style) || IsCharacter(style))
            {
                const int nextOne = (pos == len) ? GetStyleAt(pos) : GetStyleAt(pos + 1);
                if (IsCharacter(nextOne) || IsString(nextOne))
                    continue;
            }

            if (s_rightBrace.Contains(cur))
            {
                SetCaretForeground(wxColour(255, 0, 0));
                SetCaretWidth(caretWidth + 1);

                IndicatorSetForeground(s_indicHighlight, wxColour(80, 236, 120));
                IndicatorSetStyle(s_indicHighlight, wxSCI_INDIC_ROUNDBOX);
#ifndef wxHAVE_RAW_BITMAP
                IndicatorSetUnder(s_indicHighlight, true);
#endif
                SetIndicatorCurrent(s_indicHighlight);
                IndicatorFillRange(pos, 1);
                m_bracePosition = pos + 1;
                return;
            }
        }
        while (++pos);
    }

    m_bracePosition = wxSCI_INVALID_POSITION;
    m_lastPosition = wxSCI_INVALID_POSITION;
    m_tabSmartJump = false;
    SetIndicatorCurrent(s_indicHighlight);
    IndicatorClearRange(0, len);

    SetCaretForeground(caretForeground);
    SetCaretWidth(caretWidth);
}

void cbStyledTextCtrl::EmulateDwellStart()
{
    EditorBase *editor = static_cast<EditorBase*>(m_pParent);

    CodeBlocksEvent event(cbEVT_EDITOR_TOOLTIP);
    wxPoint pt(ScreenToClient(wxGetMousePosition()));
    event.SetX(pt.x);
    event.SetY(pt.y);
    int pos = PositionFromPoint(pt);
    int style = GetStyleAt(pos);
    event.SetInt(style);
    event.SetEditor(editor);
    event.SetExtraLong(0); // allow plugins to recommend each other not to cancel a tooltip
    Manager::Get()->GetPluginManager()->NotifyPlugins(event);
}

void cbStyledTextCtrl::EnableTabSmartJump(bool enable)
{
    m_tabSmartJump = enable;
    m_bracePosition = GetCurrentPos();
    HighlightRightBrace();
}

std::map<int, std::set<int> > &cbStyledTextCtrl::GetCharacterLexerStyles()
{
    return CharacterLexerStyles;
}

std::map<int, std::set<int> > &cbStyledTextCtrl::GetStringLexerStyles()
{
    return StringLexerStyles;
}

std::map<int, std::set<int> > &cbStyledTextCtrl::GetPreprocessorLexerStyles()
{
    return PreprocessorLexerStyles;
}

std::map<int, std::set<int> > &cbStyledTextCtrl::GetCommentLexerStyles()
{
    return CommentLexerStyles;
}

void cbStyledTextCtrl::MakeNearbyLinesVisible(int line)
{
    const int dist = VisibleFromDocLine(line) - GetFirstVisibleLine();
    if (dist >= 0 && dist < 2)
        LineScroll(0, dist - 2);
    else if (dist >= LinesOnScreen() - 2)
        LineScroll(0, 3 + dist - LinesOnScreen());
}
