/*
 * This file is part of the Code::Blocks IDE and licensed under the GNU General Public License, version 3
 * http://www.gnu.org/licenses/gpl-3.0.html
 *
 * $Revision: 12419 $
 * $Id: abbreviations.cpp 12419 2021-05-09 12:50:23Z fuscated $
 * $HeadURL: https://svn.code.sf.net/p/codeblocks/code/trunk/src/plugins/abbreviations/abbreviations.cpp $
 */

#include <sdk.h>

#ifndef CB_PRECOMP
    #include "cbeditor.h"
    #include "cbstyledtextctrl.h"
    #include "configmanager.h"
    #include "configurationpanel.h"
    #include "editorcolourset.h"
    #include "editormanager.h"
    #include "logmanager.h"
    #include "macrosmanager.h"
    #include "scriptingmanager.h"
#endif

#include "abbreviations.h"
#include "abbreviationsconfigpanel.h"
#include "ccmanager.h"
#include "editor_hooks.h"
#include "scripting/bindings/sc_utils.h"
#include "scripting/bindings/sc_typeinfo_all.h"

// Register the plugin with Code::Blocks.
// We are using an anonymous namespace so we don't litter the global one.
namespace
{
    PluginRegistrant<Abbreviations> reg(_T("Abbreviations"));
    const int idEditAutoComplete = wxNewId();
}

Abbreviations* Abbreviations::m_Singleton = nullptr;

// events handling
BEGIN_EVENT_TABLE(Abbreviations, cbPlugin)
    // add any events you want to handle here
    EVT_MENU(idEditAutoComplete,      Abbreviations::OnEditAutoComplete)
    EVT_UPDATE_UI(idEditAutoComplete, Abbreviations::OnEditMenuUpdateUI)
END_EVENT_TABLE()

wxString defaultLanguageStr = _T("--default--");

// constructor
Abbreviations::Abbreviations()
{
    // Make sure our resources are available.
    // In the generated boilerplate code we have no resources but when
    // we add some, it will be nice that this code is in place already ;)
    if (!Manager::LoadResource(_T("abbreviations.zip")))
        NotifyMissingFile(_T("abbreviations.zip"));

    m_IsAutoCompVisible = false;
}

// destructor
Abbreviations::~Abbreviations()
{
}

void Abbreviations::OnAttach()
{
    // do whatever initialization you need for your plugin
    // NOTE: after this function, the inherited member variable
    // m_IsAttached will be TRUE...
    // You should check for it in other functions, because if it
    // is FALSE, it means that the application did *not* "load"
    // (see: does not need) this plugin...

    //wxASSERT(m_Singleton == 0);
    m_Singleton = this;

    LoadAutoCompleteConfig();
    RegisterScripting();

    // hook to editors
    EditorHooks::HookFunctorBase* myhook = new EditorHooks::HookFunctor<Abbreviations>(this, &Abbreviations::EditorEventHook);
    m_EditorHookId = EditorHooks::RegisterHook(myhook);
}

void Abbreviations::OnRelease(cb_unused bool appShutDown)
{
    // do de-initialization for your plugin
    // if appShutDown is true, the plugin is unloaded because Code::Blocks is being shut down,
    // which means you must not use any of the SDK Managers
    // NOTE: after this function, the inherited member variable
    // m_IsAttached will be FALSE...

    UnregisterScripting();
    SaveAutoCompleteConfig();

    if (m_Singleton == this)
        m_Singleton = nullptr;

    // unregister hook
    // 'true' will delete the functor too
    EditorHooks::UnregisterHook(m_EditorHookId, true);

    ClearAutoCompLanguageMap();
}


namespace ScriptBindings
{
    /** Try to auto-complete the current word.
      *
      * This has nothing to do with code-completion plugins. Editor auto-completion
      * is a feature that saves typing common blocks of code, e.g.
      *
      * If you have typed "forb" (no quotes) and select auto-complete, then
      * it will convert "forb" to "for ( ; ; ){ }".
      * If the word up to the caret position is an unknown keyword, nothing happens.
      *
      * These keywords/code pairs can be edited in the editor configuration
      * dialog.
      */
    SQInteger CallDoAutoComplete(HSQUIRRELVM v)
    {
        // this, ed
        ExtractParams2<SkipParam, cbEditor *> extractor(v);
        if (!extractor.Process("Abbreviations::AutoComplete"))
                return extractor.ErrorMessage();
        if (Abbreviations::Get())
            Abbreviations::Get()->DoAutoComplete(extractor.p1);
        return 0;
    }
} // namespace ScriptBindings

void Abbreviations::RegisterScripting()
{
    HSQUIRRELVM vm = Manager::Get()->GetScriptingManager()->GetVM();
    if (vm)
    {
        ScriptBindings::PreserveTop preserveTop(vm);
        sq_pushroottable(vm);
        ScriptBindings::BindMethod(vm, _SC("AutoComplete"), ScriptBindings::CallDoAutoComplete,
                                   nullptr);
        sq_poptop(vm); // Pop root table.
    }
}

void Abbreviations::UnregisterScripting()
{
    HSQUIRRELVM vm = Manager::Get()->GetScriptingManager()->GetVM();
    if (vm)
    {
        ScriptBindings::PreserveTop preserveTop(vm);
        sq_pushroottable(vm);
        sq_pushstring(vm, _SC("AutoComplete"), -1);
        sq_deleteslot(vm, -2, false);
        sq_poptop(vm);
    }
}

void Abbreviations::BuildMenu(wxMenuBar* menuBar)
{
    //NOTE: Be careful in here... The application's menubar is at your disposal.

    // if not attached, exit
    if ( !IsAttached() )
        return;

    int editmenuPos = menuBar->FindMenu(_("&Edit"));
    if (editmenuPos == wxNOT_FOUND) return;
    wxMenu* editMenu = menuBar->GetMenu(editmenuPos);

    if (editMenu)
    {
        editMenu->AppendSeparator();
        editMenu->Append(idEditAutoComplete, _T("Auto-complete\tCtrl-J"), _T("Auto-completes the word under the caret (nothing to do with code-completion plugins)"));
    }
}

static int CalcStcFontSize(cbStyledTextCtrl *stc)
{
    wxFont defaultFont = stc->StyleGetFont(wxSCI_STYLE_DEFAULT);
    defaultFont.SetPointSize(defaultFont.GetPointSize() + stc->GetZoom());
    int fontSize;
    stc->GetTextExtent(wxT("A"), nullptr, &fontSize, nullptr, nullptr, &defaultFont);
    return fontSize;
}

void Abbreviations::OnEditAutoComplete(cb_unused wxCommandEvent& event)
{
    cbEditor* editor = Manager::Get()->GetEditorManager()->GetBuiltinActiveEditor();
    if (!editor)
        return;
    cbStyledTextCtrl* control = editor->GetControl();

    const AutoCompleteMap& acm = *GetCurrentACMap(editor);

    int curPos = control->GetCurrentPos();
    int startPos = control->WordStartPosition(curPos, true);
    const int endPos = control->WordEndPosition(curPos, true);

    const wxString keyword = control->GetTextRange(startPos, endPos);
    AutoCompleteMap::const_iterator acm_it = acm.find(keyword);

    if (acm_it != acm.end())
    {
        DoAutoComplete(editor);
    }
    else
    {
        wxArrayString items;
        for (acm_it = acm.begin(); acm_it != acm.end(); ++acm_it)
        {
            if (acm_it->first.Lower().StartsWith(keyword))
                items.Add(acm_it->first + _T("?0"));
        }

        if (!items.IsEmpty())
        {

            control->ClearRegisteredImages();
            {
                const int fontSize = CalcStcFontSize(control);
                const int size = cbFindMinSize16to64(fontSize);

                const wxString prefix = ConfigManager::GetDataFolder()
                      + wxString::Format(_T("/abbreviations.zip#zip:images/%dx%d/"), size, size);

                control->RegisterImage(0, cbLoadBitmap(prefix + wxT("arrow.png")));
            }

            items.Sort();
            wxString itemsStr = GetStringFromArray(items, _T(" "));
            control->AutoCompSetSeparator(_T(' '));
            control->AutoCompSetTypeSeparator(_T('?'));
            Manager::Get()->GetCCManager()->InjectAutoCompShow(endPos - startPos, itemsStr);
        }
        m_IsAutoCompVisible = control->AutoCompActive();
    }

}

void Abbreviations::OnEditMenuUpdateUI(wxUpdateUIEvent& event)
{
    cbEditor* ed = Manager::Get()->GetEditorManager()->GetBuiltinActiveEditor();
    event.Enable(ed != NULL);
}

void Abbreviations::DoAutoComplete(cbEditor* ed)
{
    if (!ed)
        return;

    cbStyledTextCtrl* control = ed->GetControl();
    if (!control)
        return;

    if (control->AutoCompActive())
        control->AutoCompCancel();

    if (control->CallTipActive())
        control->CallTipCancel();

    m_IsAutoCompVisible = false;

    LogManager* logMan = Manager::Get()->GetLogManager();
    int curPos = control->GetCurrentPos();
    int wordStartPos = control->WordStartPosition(curPos, true);
    const int endPos = control->WordEndPosition(curPos, true);
    wxString keyword = control->GetTextRange(wordStartPos, endPos);
    wxString lineIndent = ed->GetLineIndentString(control->GetCurrentLine());
    logMan->DebugLog(_T("Auto-complete keyword: ") + keyword);

    AutoCompleteMap* pAutoCompleteMap = GetCurrentACMap(ed);
    AutoCompleteMap::iterator it = pAutoCompleteMap->find(keyword);
    if (it != pAutoCompleteMap->end() )
    {
        // found; auto-complete it
        logMan->DebugLog(_T("Auto-complete match for keyword found."));

        // indent code accordingly
        wxString code = it->second;
        code.Replace(_T("\n"), _T('\n') + lineIndent);

        // look for and replace macros
        int macroPos = code.Find(_T("$("));
        while (macroPos != -1)
        {
            // locate ending parenthesis
            int macroPosEnd = macroPos + 2;
            int len = (int)code.Length();

            while (macroPosEnd < len && code.GetChar(macroPosEnd) != _T(')'))
                ++macroPosEnd;

            if (macroPosEnd == len)
                return; // no ending parenthesis

            wxString macroName = code.SubString(macroPos + 2, macroPosEnd - 1);
            logMan->DebugLog(_T("Found macro: ") + macroName);
            wxString macro = cbGetTextFromUser(_("Please enter the text for \"") + macroName + _T("\":"),
                                               _("Macro substitution"));
            if (macro.IsEmpty())
                return;

            code.Replace(_T("$(") + macroName + _T(")"), macro);
            macroPos = code.Find(_T("$("));
        }

        control->BeginUndoAction();

        // delete keyword
        control->SetSelectionVoid(wordStartPos, endPos);
        control->ReplaceSelection(wxEmptyString);
        curPos = wordStartPos;

        // replace any other macros in the generated code
        Manager::Get()->GetMacrosManager()->ReplaceMacros(code);
        // match current EOL mode
        if (control->GetEOLMode() == wxSCI_EOL_CRLF)
            code.Replace(wxT("\n"), wxT("\r\n"));
        else if (control->GetEOLMode() == wxSCI_EOL_CR)
            code.Replace(wxT("\n"), wxT("\r"));
        // add the text
        control->InsertText(curPos, code);

        // put cursor where "|" appears in code (if it appears)
        int caretPos = code.Find(_T('|'));
        if (caretPos != -1)
        {
            control->SetCurrentPos(curPos + caretPos);
            control->SetSelectionVoid(curPos + caretPos, curPos + caretPos + 1);
            control->ReplaceSelection(wxEmptyString);
        }
        control->ChooseCaretX();

        control->EndUndoAction();
    }
}

void Abbreviations::LoadAutoCompleteConfig()
{
    ClearAutoCompLanguageMap();
    AutoCompleteMap* pAutoCompleteMap;
    wxArrayString list = Manager::Get()->GetConfigManager(_T("editor"))->EnumerateSubPaths(_T("/auto_complete"));
    for (unsigned int i = 0; i < list.GetCount(); ++i)
    {
        wxString langStr = Manager::Get()->GetConfigManager(_T("editor"))->Read(_T("/auto_complete/") + list[i] + _T("/language"), defaultLanguageStr);
        wxString name = Manager::Get()->GetConfigManager(_T("editor"))->Read(_T("/auto_complete/") + list[i] + _T("/name"), wxEmptyString);
        wxString code = Manager::Get()->GetConfigManager(_T("editor"))->Read(_T("/auto_complete/") + list[i] + _T("/code"), wxEmptyString);
        if (m_AutoCompLanguageMap.find(langStr) == m_AutoCompLanguageMap.end())
            m_AutoCompLanguageMap[langStr] = new AutoCompleteMap();

        pAutoCompleteMap = m_AutoCompLanguageMap[langStr];

        if (name.IsEmpty())
            continue;
        // convert non-printable chars to printable
        wxString resolved;
        resolved.Alloc(code.Length());
        for (size_t pos = 0; pos < code.Length(); ++pos)
        {
            if (code[pos] == wxT('\\') && pos < code.Length() - 1)
            {
                ++pos;
                if (code[pos] == wxT('n'))
                    resolved += wxT("\n");
                else if (code[pos] == wxT('r')) // should not exist, remove in next step
                    resolved += wxT("\r");
                else if (code[pos] == wxT('t'))
                    resolved += wxT("\t");
                else if (code[pos] == wxT('\\'))
                    resolved += wxT("\\");
                else // ?!
                {
                    resolved += wxT("\\");
                    resolved += code[pos];
                }
            }
            else
                resolved += code[pos];
        }

        // should not exist, but remove if it does (EOL style is matched just before code generation)
        resolved.Replace(wxT("\r\n"), wxT("\n"));
        resolved.Replace(wxT("\r"),   wxT("\n"));
        (*pAutoCompleteMap)[name] = resolved;
    }

    if (m_AutoCompLanguageMap.find(defaultLanguageStr) == m_AutoCompLanguageMap.end())
        m_AutoCompLanguageMap[defaultLanguageStr] = new AutoCompleteMap();

    pAutoCompleteMap = m_AutoCompLanguageMap[defaultLanguageStr];
    if (pAutoCompleteMap->empty())
    {
        // default auto-complete items
        (*pAutoCompleteMap)[_T("if")]     = _T("if (|)\n\t;");
        (*pAutoCompleteMap)[_T("ifb")]    = _T("if (|)\n{\n\t\n}");
        (*pAutoCompleteMap)[_T("ife")]    = _T("if (|)\n{\n\t\n}\nelse\n{\n\t\n}");
        (*pAutoCompleteMap)[_T("ifei")]   = _T("if (|)\n{\n\t\n}\nelse if ()\n{\n\t\n}\nelse\n{\n\t\n}");
        (*pAutoCompleteMap)[_T("guard")]  = _T("#ifndef $(Guard token)\n#define $(Guard token)\n\n|\n\n#endif // $(Guard token)\n");
        (*pAutoCompleteMap)[_T("while")]  = _T("while (|)\n\t;");
        (*pAutoCompleteMap)[_T("whileb")] = _T("while (|)\n{\n\t\n}");
        (*pAutoCompleteMap)[_T("do")]     = _T("do\n{\n\t\n} while (|);");
        (*pAutoCompleteMap)[_T("switch")] = _T("switch (|)\n{\ncase :\n\tbreak;\n\ndefault:\n\tbreak;\n}\n");
        (*pAutoCompleteMap)[_T("for")]    = _T("for (|; ; )\n\t;");
        (*pAutoCompleteMap)[_T("forb")]   = _T("for (|; ; )\n{\n\t\n}");
        (*pAutoCompleteMap)[_T("class")]  = _T("class $(Class name)|\n{\npublic:\n\t$(Class name)();\n\t~$(Class name)();\nprotected:\nprivate:\n};\n");
        (*pAutoCompleteMap)[_T("struct")] = _T("struct |\n{\n\t\n};\n");
    }
    ExchangeTabAndSpaces(*pAutoCompleteMap);
    // date and time macros
    // these are auto-added if they 're found to be missing
    const wxString timeAndDate[9][2] =
    {
        { _T("tday"),   _T("$TDAY") },
        { _T("tdayu"),  _T("$TDAY_UTC") },
        { _T("today"),  _T("$TODAY") },
        { _T("todayu"), _T("$TODAY_UTC") },
        { _T("now"),    _T("$NOW") },
        { _T("nowl"),   _T("$NOW_L") },
        { _T("nowu"),   _T("$NOW_UTC") },
        { _T("nowlu"),  _T("$NOW_L_UTC") },
        { _T("wdu"),    _T("$WEEKDAY_UTC") },
    };

    for (int i = 0; i < 9; ++i)
    {
        if (pAutoCompleteMap->find(timeAndDate[i][0]) == pAutoCompleteMap->end())
            (*pAutoCompleteMap)[timeAndDate[i][0]] = timeAndDate[i][1];
    }

    wxString langFortran = _T("Fortran");
    if (m_AutoCompLanguageMap.find(langFortran) == m_AutoCompLanguageMap.end())
        m_AutoCompLanguageMap[langFortran] = new AutoCompleteMap();

    pAutoCompleteMap = m_AutoCompLanguageMap[langFortran];
    if (pAutoCompleteMap->empty())
    {
        // default auto-complete items for Fortran
        (*pAutoCompleteMap)[_T("if")]  = _T("if (|) then\n\t\nend if\n");
        (*pAutoCompleteMap)[_T("do")]  = _T("do |\n\t\nend do\n");
        (*pAutoCompleteMap)[_T("dw")]  = _T("do while (|)\n\t\nend do\n");
        (*pAutoCompleteMap)[_T("sc")]  = _T("select case (|)\n\tcase ()\n\t\t\n\tcase default\n\t\t\nend select\n");
        (*pAutoCompleteMap)[_T("fun")] = _T("function |()\n\t\nend function\n");
        (*pAutoCompleteMap)[_T("sub")] = _T("subroutine |()\n\t\nend subroutine\n");
        (*pAutoCompleteMap)[_T("mod")] = _T("module |\n\t\nend module\n");
        (*pAutoCompleteMap)[_T("ty")]  = _T("type |\n\t\nend type\n");
    }
    ExchangeTabAndSpaces(*pAutoCompleteMap);
}

void Abbreviations::ExchangeTabAndSpaces(AutoCompleteMap& map)
{
    const bool useTabs = Manager::Get()->GetConfigManager(_T("editor"))->ReadBool(_T("/use_tab"), false);
    const int tabSize = Manager::Get()->GetConfigManager(_T("editor"))->ReadInt(_T("/tab_size"), 4);
    const wxString tabSpace = wxString(_T(' '), tabSize);
    for (AutoCompleteMap::iterator it = map.begin(); it != map.end(); ++it)
    {
        wxString& item = it->second;
        if (useTabs)
            item.Replace(tabSpace, _T("\t"), true);
        else
            item.Replace(_T("\t"), tabSpace, true);
    }
}

void Abbreviations::SaveAutoCompleteConfig()
{
    Manager::Get()->GetConfigManager(_T("editor"))->DeleteSubPath(_T("/auto_complete"));
    AutoCompLanguageMap::iterator itlan;
    AutoCompleteMap::iterator it;
    int count = 0;
    for (itlan = m_AutoCompLanguageMap.begin(); itlan != m_AutoCompLanguageMap.end(); ++itlan)
    {
        wxString langStr = itlan->first;
        wxString langStrLw = langStr.Lower();
        AutoCompleteMap* pAutoCompleteMap = itlan->second;
        for (it = pAutoCompleteMap->begin(); it != pAutoCompleteMap->end(); ++it)
        {
            wxString code = it->second;
            // convert non-printable chars to printable
            code.Replace(_T("\\"),   _T("\\\\"));
            code.Replace(_T("\r\n"), _T("\\n")); // EOL style will be matched just before code generation
            code.Replace(_T("\n"),   _T("\\n"));
            code.Replace(_T("\r"),   _T("\\n"));
            code.Replace(_T("\t"),   _T("\\t"));

            ++count;
            wxString key;
            if (!langStr.IsSameAs(defaultLanguageStr))
            {
                key.Printf(_T("/auto_complete/entry%d/language"), count);
                Manager::Get()->GetConfigManager(_T("editor"))->Write(key, langStr);
            }
            key.Printf(_T("/auto_complete/entry%d/name"), count);
            Manager::Get()->GetConfigManager(_T("editor"))->Write(key, it->first);
            key.Printf(_T("/auto_complete/entry%d/code"), count);
            Manager::Get()->GetConfigManager(_T("editor"))->Write(key, code);
        }
    }
}

void Abbreviations::EditorEventHook(cbEditor* editor, wxScintillaEvent& event)
{
    cbStyledTextCtrl* control = editor->GetControl();

    if ( !IsAttached() || !m_IsAutoCompVisible || !control )
    {
        event.Skip();
        return;
    }

    if (event.GetEventType() == wxEVT_SCI_AUTOCOMP_SELECTION)
    {
        const wxString& itemText = event.GetText();
        int curPos = control->GetCurrentPos();
        int startPos = control->WordStartPosition(curPos, true);
        const int endPos = control->WordEndPosition(curPos, true);

        control->BeginUndoAction();
        control->SetTargetStart(startPos);
        control->SetTargetEnd(endPos);
        control->ReplaceTarget(itemText);
        control->GotoPos(startPos + itemText.size() );
        control->EndUndoAction();

        DoAutoComplete(editor);

        // prevent other plugins from insertion this keyword
        event.SetText(wxEmptyString);
        event.SetEventType(wxEVT_NULL);
    }
    else // here should be: else if (event.GetEventType() == wxEVT_SCI_AUTOCOMP_CANCELLED)
    {    // but is this event doesn't occur.
        m_IsAutoCompVisible = control->AutoCompActive();
    }

    if (!m_IsAutoCompVisible)
      event.Skip(); // allow others to handle this event
}

cbConfigurationPanel* Abbreviations::GetConfigurationPanel(wxWindow* parent)
{
    return new AbbreviationsConfigPanel(parent, this);
}

void Abbreviations::ClearAutoCompLanguageMap()
{
    AutoCompLanguageMap::iterator it;
    for (it=m_AutoCompLanguageMap.begin(); it!=m_AutoCompLanguageMap.end(); ++it)
    {
        it->second->clear();
        delete it->second;
        it->second = 0;
    }
    m_AutoCompLanguageMap.clear();
}

AutoCompleteMap* Abbreviations::GetCurrentACMap(cbEditor* ed)
{
    AutoCompleteMap* pAutoCompleteMap;
    EditorColourSet* colour_set = ed->GetColourSet();
    if (colour_set)
    {
        wxString strLang = colour_set->GetLanguageName(ed->GetLanguage());

        if (strLang == _T("Fortran77")) // the same abbreviations for Fortran and Fortran77
            strLang = _T("Fortran");

        if (m_AutoCompLanguageMap.find(strLang) == m_AutoCompLanguageMap.end())
            pAutoCompleteMap = m_AutoCompLanguageMap[defaultLanguageStr];
        else
            pAutoCompleteMap = m_AutoCompLanguageMap[strLang];
    }
    else
        pAutoCompleteMap = m_AutoCompLanguageMap[defaultLanguageStr];

    return pAutoCompleteMap;
}
