/*
* This file is part of wxSmith plugin for Code::Blocks Studio
* Copyright (C) 2006-2007  Bartlomiej Swiecki
*
* wxSmith is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* wxSmith is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with wxSmith. If not, see <http://www.gnu.org/licenses/>.
*
* $Revision: 12828 $
* $Id: wxscoder.cpp 12828 2022-06-03 14:28:11Z wh11204 $
* $HeadURL: https://svn.code.sf.net/p/codeblocks/code/trunk/src/plugins/contrib/wxSmith/wxscoder.cpp $
*/

#include "wxscoder.h"

#include <manager.h>
#include <editormanager.h>
#include <configmanager.h>
#include <logmanager.h>
#include <filemanager.h>
#include <encodingdetector.h>
#include <globals.h>
#include <wx/file.h>
#include <wx/intl.h>
#include "cbstyledtextctrl.h"
#include <editormanager.h>
#include <projectmanager.h>
#include <pluginmanager.h>
#include "sdk_events.h"

//namespace
//{
//    EncodingDetector* LoadFile(const wxString& FileName)
//    {
//        if ( !wxFileExists(FileName) ) return nullptr;
//
//        LoaderBase* Loader = FileManager::Get()->Load(FileName,false);
//        if ( !Loader ) return nullptr;
//
//        EncodingDetector* Detector = new EncodingDetector(Loader);
//        delete Loader;
//
//        if ( !Detector->IsOK() )
//        {
//            delete Detector;
//            return nullptr;
//        }
//
//        return Detector;
//    }
//}

static wxsCoder SingletonObject;
wxsCoder* wxsCoder::Singleton = &SingletonObject;

wxsCoder::wxsCoder()
{
    FlushTimer.SetOwner(this,1);
    Connect(wxEVT_TIMER,(wxObjectEventFunction)&wxsCoder::FlushTimerEvent);
}

wxsCoder::~wxsCoder()
{
    FlushAll();
}

void wxsCoder::AddCode(const wxString& FileName,const wxString& Header,const wxString& End,const wxString& Code,bool Immediately,bool CodeHasHeader,bool CodeHasEnd)
{
    wxMutexLocker Lock(DataMutex);

    const wxString FixedFileName(NormalizeFileName(FileName));
    if (FixedFileName.empty())
    {
        return;
    }

    // Find changing file
    int Index = CodeChangesFiles.Index(FileName);
    if (Index == wxNOT_FOUND)
    {
        Index = CodeChangesFiles.Count();
        CodeChangesFiles.Add(FileName);
        CodeChanges.Add(0);
    }

    // Add entry to list of waiting changes
    CodeChange* Change = new CodeChange;
    Change->Header = Header;
    Change->End = End;
    Change->Code = Code;
    Change->CodeHasHeader = CodeHasHeader;
    Change->CodeHasEnd = CodeHasEnd;
    Change->Next = CodeChanges[Index];
    CodeChanges[Index] = Change;

    // If the change has already been put onto queue, delete it
    for (CodeChange *Prev = Change, *This=Prev->Next; This; Prev = This, This = This->Next)
    {
        if (This->Header == Header && This->End == End)
        {
            Prev->Next = This->Next;
            delete This;
            This = Prev;
        }
    }

    if (Immediately)
    {
        FlushFile(FixedFileName);
    }
}

wxString wxsCoder::GetCode(const wxString& FileName,const wxString& Header,const wxString& End,bool IncludeHeader,bool IncludeEnd)
{
    wxMutexLocker Lock(DataMutex);

    wxString FixedFileName = NormalizeFileName(FileName);
    FlushFile(FixedFileName);

    const int TabSize = Manager::Get()->GetConfigManager("editor")->ReadInt("/tab_size", 4);

    // Checking if editor is opened
    EditorManager* EM = Manager::Get()->GetEditorManager();
    assert (EM != 0);
    cbEditor* Editor = EM->GetBuiltinEditor(FixedFileName);

    if (Editor)
    {
        cbStyledTextCtrl* Ctrl = Editor->GetControl();
        Ctrl->SetSearchFlags(wxSCI_FIND_MATCHCASE);
        Ctrl->SetTargetStart(0);
        Ctrl->SetTargetEnd(Ctrl->GetLength());
        int Position = Ctrl->SearchInTarget(Header);
        if (Position == -1)
            return "";

        // Counting number of indentation spaces which will be removed at
        // the beginning of each line
        int SpacesCut = 0;
        int SpacesPos = Position;
        while (--SpacesPos >= 0)
        {
            const wxChar ch = Ctrl->GetCharAt(SpacesPos);
            if (ch == '\t')
                SpacesCut += TabSize;
            else if ((ch == '\n') || (ch == '\r'))
                break;
            else
                SpacesCut++;
        }

        Ctrl->SetTargetStart(Position);
        Ctrl->SetTargetEnd(Ctrl->GetLength());
        int EndPosition = Ctrl->SearchInTarget(End);
        if (EndPosition == -1)
            return "";

        // Fixing up positions to include / exclude header and/or ending sequence
        if (!IncludeHeader)
            Position += Header.Length();

        if (IncludeEnd)
            EndPosition += End.Length();

        return CutSpaces(Ctrl->GetTextRange(Position, EndPosition), SpacesCut);
    }
    else
    {
        EncodingDetector Detector(FixedFileName);
        if (!Detector.IsOK())
            return "";

        wxString Content = Detector.GetWxStr();

        int Position = Content.First(Header);
        if (Position == -1)
            return "";

        int SpacesCut = 0;
        int SpacesPos = Position;
        while (--SpacesPos >= 0)
        {
            const wxChar ch = Content.GetChar(SpacesPos);
            if (ch == '\t')
                SpacesCut += TabSize;
            else if ((ch == '\n') || (ch == '\r'))
                break;
            else
                SpacesCut++;
        }

        if (!IncludeHeader)
            Position += Header.Length();

        Content.Remove(0, Position);
        int EndPosition = Content.First(End);
        if (EndPosition == -1)
            return "";

        if (IncludeEnd)
            EndPosition += End.Length();

        Content.Remove(EndPosition);
        return CutSpaces(Content, SpacesCut);
    }
}

wxString wxsCoder::GetFullCode(const wxString& FileName,wxFontEncoding& Encoding,bool &UseBOM)
{
    wxMutexLocker Lock(DataMutex);

    wxString FixedFileName = NormalizeFileName(FileName);
    FlushFile(FixedFileName);

    // Checking if editor is opened
    EditorManager* EM = Manager::Get()->GetEditorManager();
    assert (EM != 0);
    cbEditor* Editor = EM->GetBuiltinEditor(FixedFileName);

    if (Editor)
    {
        Encoding = Editor->GetEncoding();
        UseBOM = Editor->GetUseBom();
        cbStyledTextCtrl* Ctrl = Editor->GetControl();
        return Ctrl->GetText();
    }
    else
    {
        EncodingDetector Detector(FixedFileName);
        Encoding = Detector.GetFontEncoding();
        UseBOM = Detector.GetBOMSizeInBytes() > 0;
        return Detector.IsOK() ? Detector.GetWxStr() : wxString();
    }
}

void wxsCoder::PutFullCode(const wxString& FileName,const wxString& Code,wxFontEncoding Encoding,bool UseBOM)
{
    wxMutexLocker Lock(DataMutex);

    wxString FixedFileName = NormalizeFileName(FileName);
    int Index = CodeChangesFiles.Index(FixedFileName);
    if (Index != wxNOT_FOUND)
    {
        for (CodeChange* Change = CodeChanges[Index]; Change; )
        {
            CodeChange* Next = Change->Next;
            delete Change;
            Change = Next;
        }

        CodeChanges[Index] = 0;
    }

    // Searching for file in opened file list
    EditorManager* EM = Manager::Get()->GetEditorManager();
    assert (EM != 0);
    cbEditor* Editor = EM->GetBuiltinEditor(FixedFileName);

    if (Editor)
    {
        Editor->GetControl()->SetText(Code);
    }
    else
    {
        if (!cbSaveToFile(FixedFileName, Code, Encoding, UseBOM))
        {
            Manager::Get()->GetLogManager()->Log(wxString::Format(_("wxSmith: Couldn't write file '%s'"), FixedFileName));
        }
    }
}

void wxsCoder::FlushFile(const wxString& FileName)
{
    const int Index = CodeChangesFiles.Index(FileName);
    if (Index == wxNOT_FOUND)
        return;

    CodeChange* Changes = CodeChanges[Index];
    if (!Changes)
        return;

    // Searching for file in opened file list
    EditorManager* EM = Manager::Get()->GetEditorManager();
    assert (EM != 0);
    cbEditor* Editor = EM->GetBuiltinEditor(FileName);

    if (Editor)
    {
        wxString EOL;
        while (Changes)
        {
            CodeChange* Next = Changes->Next;
            ApplyChangesEditor(Editor, Changes->Header, Changes->End, Changes->Code, Changes->CodeHasHeader, Changes->CodeHasEnd, EOL);
            delete Changes;
            Changes = Next;
        }
    }
    else
    {
        // Reading file content
        wxString EOL;
        bool HasChanged = false;

        //wxStopWatch SW;
        EncodingDetector Detector(FileName);
        if (!Detector.IsOK())
        {
            Manager::Get()->GetLogManager()->Log(wxString::Format(_("wxSmith: Couldn't open and properly read file '%s'"), FileName));
            return;
        }
        //Manager::Get()->GetLogManager()->DebugLog(F(_T("File read time: %d ms"),SW.Time()));

        wxString Content = Detector.GetWxStr();
        while (Changes)
        {
            CodeChange* Next = Changes->Next;
            ApplyChangesString(Content,Changes->Header,Changes->End,Changes->Code,Changes->CodeHasHeader,Changes->CodeHasEnd,HasChanged,EOL);
            delete Changes;
            Changes = Next;
        }

        if (HasChanged)
        {
            // Storing the result
            //wxStopWatch SW;
            if (!cbSaveToFile(FileName, Content, Detector.GetFontEncoding(), Detector.GetBOMSizeInBytes() > 0))
            {
                Manager::Get()->GetLogManager()->Log(wxString::Format(_("wxSmith: Couldn't write data to file '%s'"), FileName));
            }
            else
            {
                CodeBlocksEvent event(cbEVT_PROJECT_FILE_CHANGED);
                event.SetString(FileName);
                Manager::Get()->GetPluginManager()->NotifyPlugins(event);
            }
            //Manager::Get()->GetLogManager()->DebugLog(F(_T("File write time: %d ms"),SW.Time()));
        }
    }

    CodeChanges[Index] = 0;
}

bool wxsCoder::ApplyChangesEditor(cbEditor* Editor,const wxString& Header,const wxString& End,wxString& Code,bool CodeHasHeader,bool CodeHasEnd,wxString& EOL)
{
    cbStyledTextCtrl* Ctrl = Editor->GetControl();
    const int FullLength = Ctrl->GetLength();

    if (EOL.empty())
    {
        // Detecting EOL style in source
        for (int i = 0; i < FullLength; ++i)
        {
            const wxChar ch = Ctrl->GetCharAt(i);
            if (ch == '\n' || ch == '\r')
            {
                EOL = ch;
                if (++i < FullLength)
                {
                    const wxChar ch2 = Ctrl->GetCharAt(i);
                    if ((ch2 == '\n' || ch2 == '\r') && ch != ch2)
                    {
                        EOL.Append(ch2);
                    }
                }
                break;
            }
        }
    }

    // Searching for beginning of section to replace
    Ctrl->SetSearchFlags(wxSCI_FIND_MATCHCASE);
    Ctrl->SetTargetStart(0);
    Ctrl->SetTargetEnd(FullLength);
    int Position = Ctrl->SearchInTarget(Header);

    if (Position == -1)
    {
        Manager::Get()->GetLogManager()->DebugLog(wxString::Format(_("wxSmith: Couldn't find code with header:\n\t\"%s\"\nin file '%s'"),
                                                                   Header,
                                                                   Editor->GetFilename()));
        return false;
    }

    // Beginning of this code block is in Position, now searching for end
    Ctrl->SetTargetStart(Position);
    Ctrl->SetTargetEnd(FullLength);
    int EndPosition = Ctrl->SearchInTarget(End);
    if (EndPosition == -1)
    {
        Manager::Get()->GetLogManager()->DebugLog(wxString::Format(_("wxSmith: Unfinished block of auto-generated code with header:\n\t\"%s\"\nin file '%s'"),
                                                                   Header,
                                                                   Editor->GetFilename()));

        return false;
    }

    // Fetching indentation
    wxString BaseIndentation;
    int IndentPos = Position;
    while (--IndentPos >= 0)
    {
        const wxChar ch = Ctrl->GetCharAt(IndentPos);
        if ((ch == '\n') || (ch == '\r'))
            break;
    }

    while (++IndentPos < Position)
    {
        const wxChar ch = Ctrl->GetCharAt(IndentPos);
        BaseIndentation.Append((ch == '\t') ? '\t' : ' ');
    }

    Code = RebuildCode(BaseIndentation, Code.c_str(), (int)Code.Length(), EOL);

    // Fixing up positions to contain or not header / ending sequence
    if (!CodeHasHeader)
        Position += Header.Length();

    if (CodeHasEnd)
        EndPosition += End.Length();

    // Checking of code has really changed
    if (Ctrl->GetTextRange(Position,EndPosition) == Code)
    {
        return true;
    }

    // Make sure that the code we're replacing is not folded. Otherwise scintilla
    // does some weird things

    const int lineEnd = Ctrl->LineFromPosition(EndPosition);
    for (int line = Ctrl->LineFromPosition(Position); line <= lineEnd; line++)
    {
        Ctrl->EnsureVisible(line);
    }

    // Replacing code
    Ctrl->SetTargetStart(Position);
    Ctrl->SetTargetEnd(EndPosition);
    Ctrl->ReplaceTarget(Code);
    Editor->SetModified();
    return true;
}

bool wxsCoder::ApplyChangesString(wxString& BaseContent,const wxString& Header,const wxString& End,wxString& Code,bool CodeHasHeader,bool CodeHasEnd,bool& HasChanged,wxString& EOL)
{
    wxString Content = BaseContent;
    if (EOL.empty())
    {
        // Detecting EOL in this sources
        for (size_t i = 0; i < Content.Length(); i++)
        {
            const wxChar ch = Content.GetChar(i);
            if (ch == '\n' || ch == '\r')
            {
                EOL = ch;
                if (++i < Content.Length())
                {
                    const wxChar ch2 = Content.GetChar(i);
                    if ((ch2 == '\n' || ch2 == '\r') && ch != ch2)
                    {
                        EOL.Append(ch2);
                    }
                }

                break;
            }
        }
    }

    // Search for header
    int Position = Content.First(Header);

    if (Position == -1)
    {
        Manager::Get()->GetLogManager()->DebugLog(wxString::Format(_("wxSmith: Couldn't find code with header:\n\t\"%s\""), Header));
        return false;
    }

    // Skipping header if necessary
    int IndentPos = Position;
    int IndentMax = Position;
    if (!CodeHasHeader)
        Position += Header.Length();

    wxString Result = Content.Left(Position);
    Content.Remove(0,Position);

    int EndPosition = Content.First(End);
    if (EndPosition == -1)
    {
        Manager::Get()->GetLogManager()->DebugLog(wxString::Format(_("wxSmith: Unfinished block of auto-generated code with header:\n\t\"%s\""), Header));
        return false;
    }

    // Including ending sequence if necessary
    if (CodeHasEnd)
        EndPosition += End.Length();

    // Fetching indentation
    wxString BaseIndentation;
    while (--IndentPos >= 0)
    {
        const wxChar ch = Result.GetChar(IndentPos);
        if ((ch == '\n') || (ch == '\r'))
            break;
    }

    while (++IndentPos < IndentMax)
    {
        const wxChar ch = Result.GetChar(IndentPos);
        BaseIndentation.Append((ch == '\t') ? '\t' : ' ');
    }

    Code = RebuildCode(BaseIndentation,Code.c_str(),Code.Length(),EOL);

    // Checking if code has really changed
    if (Content.Mid(0, EndPosition) == Code)
    {
        return true;
    }

    HasChanged = true;
    Result += Code;
    Result += Content.Remove(0, EndPosition);
    BaseContent = Result;

    return true;
}

wxString wxsCoder::RebuildCode(wxString& BaseIndentation, const wxChar* Code, int CodeLen, wxString& EOL)
{
    wxString Tab;
    ConfigManager *CfgMan = Manager::Get()->GetConfigManager("editor");
    const bool UseTab = CfgMan->ReadBool("/use_tab", false);
    if (!UseTab)
    {
        const int TabSize = CfgMan->ReadInt("/tab_size", 4);
        Tab.Append(' ', TabSize);
    }

    if (EOL.empty())
        EOL = GetEOLStr();

    BaseIndentation.Prepend(EOL);

    wxString Result;
    Result.reserve(CodeLen+10);

    while (*Code)
    {
        switch (*Code)
        {
            case '\n':
                while (!Result.empty() && (Result.Last() == ' ' || Result.Last() == '\t'))
                    Result.RemoveLast();

                Result << BaseIndentation;
                break;
            case '\t':
                if (UseTab)
                    Result << *Code;
                else
                    Result << Tab;

                break;
            default:
                Result << *Code;
        }

        Code++;
    }

    return Result;
}

wxString wxsCoder::CutSpaces(wxString Code,int Count)
{
    int TabSize = Manager::Get()->GetConfigManager("editor")->ReadInt("/tab_size", 4);
    if (TabSize < 1)
        TabSize = 4;

    // Changing to \n line end mode
    wxString Result;

    for (;;)
    {
        const int PosN = Code.Find("\n");
        const int PosR = Code.Find("\r");

        if ((PosN < 0) && (PosR < 0))
            break;

        int Pos;
        if (PosN < 0)
            Pos = PosR;
        else if (PosR < 0)
            Pos = PosN;
        else
            Pos = (PosN < PosR) ? PosN : PosR;

        Result.Append(Code.Left(Pos));
        Code.Remove(0,Pos);
        while (Code.Length())
        {
            if ((Code[0] != '\n') && (Code[0] != '\r'))
                break;

            Code.Remove(0, 1);
        }

        int LeftSpaces = Count;
        while (!Code.empty() && LeftSpaces > 0)
        {
            if (Code[0] == ' ')
                LeftSpaces--;
            else if (Code[0] == '\t')
                LeftSpaces -= TabSize;
            else
                break;

            Code.Remove(0, 1);
        }

        Result.Append('\n');
    }

    Result.Append(Code);
    return Result;
}

wxString wxsCoder::NormalizeFileName(const wxString& FileName)
{
    // Updating the file name in case there are some ".." or "." enteries which prevents from finding
    // opened editor for file
    wxFileName FixedNameObject(FileName);
    FixedNameObject.Normalize(wxPATH_NORM_DOTS);
    return FixedNameObject.GetFullPath();
}

void wxsCoder::Flush(int Delay)
{
    if (Delay <= 0)
    {
        FlushTimer.Stop();
        FlushAll();
    }
    else
    {
        FlushTimer.Start(Delay,true);
    }
}

void wxsCoder::FlushAll()
{
    //wxStopWatch SW;
    for (int i = 0; i < (int)CodeChangesFiles.Count(); i++)
    {
        FlushFile(CodeChangesFiles[i]);
    }

    CodeChanges.Clear();
    CodeChangesFiles.Clear();
    //Manager::Get()->GetLogManager()->DebugLog(F(_T("wxSmith: Flushing of code done in %d ms"),SW.Time()));
}

void wxsCoder::FlushTimerEvent(cb_unused wxTimerEvent& event)
{
    FlushAll();
}
