// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under    *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0          *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************

#include "log_panel.h"
#include <wx+/window_tools.h>
#include <wx+/image_resources.h>
#include <wx+/rtl.h>
#include <wx+/context_menu.h>
#include <wx+/popup_dlg.h>

using namespace zen;
using namespace fff;


namespace
{
inline wxColor getColorGridLine() { return {192, 192, 192}; } //light grey


inline
wxImage getImageButtonPressed(const char* imageName)
{
    return layOver(loadImage("msg_button_pressed"),
                   loadImage(imageName));
}


inline
wxImage getImageButtonReleased(const char* imageName)
{
    return greyScale(loadImage(imageName));
    //loadImage(utfTo<wxString>(imageName)).ConvertToGreyscale(1.0/3, 1.0/3, 1.0/3); //treat all channels equally!
    //brighten(output, 30);
}


enum class ColumnTypeLog
{
    time,
    severity,
    text,
};
}


//a vector-view on ErrorLog considering multi-line messages: prepare consumption by Grid
class fff::MessageView
{
public:
    explicit MessageView(const SharedRef<const ErrorLog>& log) : log_(log) {}

    size_t rowsOnView() const { return viewRef_.size(); }

    struct LogEntryView
    {
        time_t      time = 0;
        MessageType type = MSG_TYPE_INFO;
        std::string_view messageLine;
        bool firstLine = false; //if LogEntry::message spans multiple rows
    };

    std::optional<LogEntryView> getEntry(size_t row) const
    {
        if (row < viewRef_.size())
        {
            const Line& line = viewRef_[row];

            LogEntryView output;
            output.time = line.logIt->time;
            output.type = line.logIt->type;
            output.messageLine = extractLine(line.logIt->message, line.row);
            output.firstLine = line.row == 0; //this is virtually always correct, unless first line of the original message is empty!
            return output;
        }
        return {};
    }

    void updateView(int includedTypes) //MSG_TYPE_INFO | MSG_TYPE_WARNING, etc. see error_log.h
    {
        viewRef_.clear();

        for (auto it = log_.ref().begin(); it != log_.ref().end(); ++it)
            if (it->type & includedTypes)
            {
                assert(!startsWith(it->message, '\n'));

                size_t rowNumber = 0;
                bool lastCharNewline = true;
                for (const char c : it->message)
                    if (c == '\n')
                    {
                        if (!lastCharNewline) //do not reference empty lines!
                            viewRef_.push_back({it, rowNumber});
                        ++rowNumber;
                        lastCharNewline = true;
                    }
                    else
                        lastCharNewline = false;

                if (!lastCharNewline)
                    viewRef_.push_back({it, rowNumber});
            }
    }

private:
    static std::string_view extractLine(const Zstringc& message, size_t textRow)
    {
        auto it1 = message.begin();
        for (;;)
        {
            auto it2 = std::find_if(it1, message.end(), [](const char c) { return c == '\n'; });
            if (textRow == 0)
                return {it1, makeUnsigned(it2 - it1)};

            if (it2 == message.end())
            {
                assert(false);
                return {it1, 0};
            }

            it1 = it2 + 1; //skip newline
            --textRow;
        }
    }

    struct Line
    {
        ErrorLog::const_iterator logIt; //always bound!
        size_t row; //LogEntry::message may span multiple rows
    };

    std::vector<Line> viewRef_; //partial view on log_
    /*          /|\
                 | updateView()
                 |                      */
    const SharedRef<const ErrorLog> log_;
};

//-----------------------------------------------------------------------------
namespace
{
//Grid data implementation referencing MessageView
class GridDataMessages : public GridData
{
public:
    explicit GridDataMessages(const SharedRef<const ErrorLog>& log) : msgView_(log) {}

    MessageView& getDataView() { return msgView_; }

    size_t getRowCount() const override { return msgView_.rowsOnView(); }

    std::wstring getValue(size_t row, ColumnType colType) const override
    {
        if (const std::optional<MessageView::LogEntryView> entry = msgView_.getEntry(row))
            switch (static_cast<ColumnTypeLog>(colType))
            {
                case ColumnTypeLog::time:
                    if (entry->firstLine)
                        return utfTo<std::wstring>(formatTime(formatTimeTag, getLocalTime(entry->time))); //empty string on error
                    break;

                case ColumnTypeLog::severity:
                    if (entry->firstLine)
                        switch (entry->type)
                        {
                            case MSG_TYPE_INFO:
                                return _("Info");
                            case MSG_TYPE_WARNING:
                                return _("Warning");
                            case MSG_TYPE_ERROR:
                                return _("Error");
                        }
                    break;

                case ColumnTypeLog::text:
                    return utfTo<std::wstring>(entry->messageLine);
            }
        return std::wstring();
    }

    void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, HoverArea rowHover) override
    {
        if (!enabled || !selected)
            ; //clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); -> already the default
        else
            GridData::renderRowBackgound(dc, rect, row, true /*enabled*/, true /*selected*/, rowHover);

        //-------------- draw item separation line -----------------
        const bool drawBottomLine = [&] //don't separate multi-line messages
        {
            if (std::optional<MessageView::LogEntryView> nextEntry = msgView_.getEntry(row + 1))
                return nextEntry->firstLine;
            return true;
        }();

        if (drawBottomLine)
        drawRectangleBorder(dc, rect, getColorGridLine(), dipToWxsize(1), wxBOTTOM);
        //--------------------------------------------------------
    }

    void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override
    {
        wxDCTextColourChanger textColor(dc);
        if (enabled && selected) //accessibility: always set *both* foreground AND background colors!
            textColor.Set(*wxBLACK);

        wxRect rectTmp = rect;

        if (std::optional<MessageView::LogEntryView> entry = msgView_.getEntry(row))
            switch (static_cast<ColumnTypeLog>(colType))
            {
                case ColumnTypeLog::time:
                    drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_CENTER);
                    break;

                case ColumnTypeLog::severity:
                    if (entry->firstLine)
                    {
                        wxImage msgTypeIcon = [&]
                        {
                            switch (entry->type)
                            {
                                case MSG_TYPE_INFO:
                                    return loadImage("msg_info", dipToScreen(getMenuIconDipSize()));
                                case MSG_TYPE_WARNING:
                                    return loadImage("msg_warning", dipToScreen(getMenuIconDipSize()));
                                case MSG_TYPE_ERROR:
                                    return loadImage("msg_error", dipToScreen(getMenuIconDipSize()));
                            }
                            assert(false);
                            return wxNullImage;
                        }();
                        drawBitmapRtlNoMirror(dc, enabled ? msgTypeIcon : msgTypeIcon.ConvertToDisabled(), rectTmp, wxALIGN_CENTER);
                    }
                    break;

                case ColumnTypeLog::text:
                    rectTmp.x     += getColumnGapLeft();
                    rectTmp.width -= getColumnGapLeft();
                    drawCellText(dc, rectTmp, getValue(row, colType));
                    break;
            }
    }

    int getBestSize(const wxReadOnlyDC& dc, size_t row, ColumnType colType) override
    {
        // -> synchronize renderCell() <-> getBestSize()

        if (msgView_.getEntry(row))
            switch (static_cast<ColumnTypeLog>(colType))
            {
                case ColumnTypeLog::time:
                    return 2 * getColumnGapLeft() + dc.GetTextExtent(getValue(row, colType)).GetWidth();

                case ColumnTypeLog::severity:
                    return dipToWxsize(getMenuIconDipSize());

                case ColumnTypeLog::text:
                    return getColumnGapLeft() + dc.GetTextExtent(getValue(row, colType)).GetWidth();
            }
        return 0;
    }

    static int getColumnTimeDefaultWidth(Grid& grid)
    {
        wxInfoDC dc(&grid.getMainWin());
        dc.SetFont(grid.getMainWin().GetFont());
        return 2 * getColumnGapLeft() + dc.GetTextExtent(utfTo<wxString>(formatTime(formatTimeTag))).GetWidth();
    }

    static int getColumnSeverityDefaultWidth()
    {
        return dipToWxsize(getMenuIconDipSize());
    }

    static int getRowDefaultHeight(const Grid& grid)
    {
        return std::max(dipToWxsize(getMenuIconDipSize()), grid.getMainWin().GetCharHeight() + dipToWxsize(2) /*extra space*/) + dipToWxsize(1) /*bottom border*/;
    }

    std::wstring getToolTip(size_t row, ColumnType colType, HoverArea rowHover) override
    {
        switch (static_cast<ColumnTypeLog>(colType))
        {
            case ColumnTypeLog::time:
            case ColumnTypeLog::text:
                break;

            case ColumnTypeLog::severity:
                return getValue(row, colType);
        }
        return std::wstring();
    }

    std::wstring getColumnLabel(ColumnType colType) const override { return std::wstring(); }

private:
    MessageView msgView_;
};
}

//########################################################################################

LogPanel::LogPanel(wxWindow* parent) : LogPanelGenerated(parent)
{
    const int rowHeight           = GridDataMessages::getRowDefaultHeight(*m_gridMessages);
    const int colMsgTimeWidth     = GridDataMessages::getColumnTimeDefaultWidth(*m_gridMessages);
    const int colMsgSeverityWidth = GridDataMessages::getColumnSeverityDefaultWidth();

    m_gridMessages->setColumnLabelHeight(0);
    m_gridMessages->showRowLabel(false);
    m_gridMessages->setRowHeight(rowHeight);
    m_gridMessages->setColumnConfig(
    {
        {static_cast<ColumnType>(ColumnTypeLog::time    ), colMsgTimeWidth,                        0, true},
        {static_cast<ColumnType>(ColumnTypeLog::severity), colMsgSeverityWidth,                    0, true},
        {static_cast<ColumnType>(ColumnTypeLog::text    ), -colMsgTimeWidth - colMsgSeverityWidth, 1, true},
    });

    //support for CTRL + C
    m_gridMessages->Bind(wxEVT_KEY_DOWN,          [this](wxKeyEvent& event) { onGridKeyEvent(event); });

    m_gridMessages->Bind(EVENT_GRID_CONTEXT_MENU, [this](GridContextMenuEvent& event) { onMsgGridContext(event); });

    Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events

    setLog(nullptr);
}


void LogPanel::setLog(const std::shared_ptr<const ErrorLog>& log)
{
    SharedRef<const ErrorLog> newLog = [&]
    {
        if (log)
            return SharedRef<const ErrorLog>(log);

        ErrorLog dummyLog;
        logMsg(dummyLog, _("No log entries"), MSG_TYPE_INFO);
        return makeSharedRef<const ErrorLog>(std::move(dummyLog));
    }();

    const ErrorLogStats logCount = getStats(newLog.ref());

    auto initButton = [](ToggleButton& btn, const char* imgName, const wxString& tooltip)
    {
        btn.init(getImageButtonPressed(imgName), getImageButtonReleased(imgName));
        btn.SetToolTip(tooltip);
    };

    initButton(*m_bpButtonErrors,   "msg_error",   _("Error"  ) + L" (" + formatNumber(logCount.errors)   + L')');
    initButton(*m_bpButtonWarnings, "msg_warning", _("Warning") + L" (" + formatNumber(logCount.warnings) + L')');
    initButton(*m_bpButtonInfo,     "msg_info",    _("Info"   ) + L" (" + formatNumber(logCount.infos)    + L')');

    m_bpButtonErrors  ->setActive(true);
    m_bpButtonWarnings->setActive(true);
    m_bpButtonInfo    ->setActive(logCount.warnings + logCount.errors == 0);

    m_bpButtonErrors  ->Show(logCount.errors   != 0);
    m_bpButtonWarnings->Show(logCount.warnings != 0);
    m_bpButtonInfo    ->Show(logCount.infos    != 0);

    m_gridMessages->setDataProvider(std::make_shared<GridDataMessages>(newLog));

    updateGrid();
}


MessageView& LogPanel::getDataView()
{
    if (auto* prov = dynamic_cast<GridDataMessages*>(m_gridMessages->getDataProvider()))
        return prov->getDataView();
    throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] m_gridMessages was not initialized.");
}


void LogPanel::updateGrid()
{
    int includedTypes = 0;
    if (m_bpButtonErrors->isActive())
        includedTypes |= MSG_TYPE_ERROR;

    if (m_bpButtonWarnings->isActive())
        includedTypes |= MSG_TYPE_WARNING;

    if (m_bpButtonInfo->isActive())
        includedTypes |= MSG_TYPE_INFO;

    getDataView().updateView(includedTypes); //update MVC "model"
    m_gridMessages->Refresh();               //update MVC "view"
}

void LogPanel::onErrors(wxCommandEvent& event)
{
    m_bpButtonErrors->toggle();
    updateGrid();
}


void LogPanel::onWarnings(wxCommandEvent& event)
{
    m_bpButtonWarnings->toggle();
    updateGrid();
}


void LogPanel::onInfo(wxCommandEvent& event)
{
    m_bpButtonInfo->toggle();
    updateGrid();
}


void LogPanel::onMsgGridContext(GridContextMenuEvent& event)
{
    const std::vector<size_t> selection = m_gridMessages->getSelectedRows();

    const size_t rowCount = [&] -> size_t
    {
        if (auto prov = m_gridMessages->getDataProvider())
            return prov->getRowCount();
        return 0;
    }();

    ContextMenu menu;
    menu.addItem(_("&Copy") + L"\tCtrl+C", [this] { copySelectionToClipboard(); }, loadImage("item_copy_sicon"), !selection.empty());
    menu.addSeparator();

    menu.addItem(_("Select all") + L"\tCtrl+A", [this] { m_gridMessages->selectAllRows(GridEventPolicy::allow); }, wxNullImage, rowCount > 0);

    menu.popup(*m_gridMessages, event.mousePos_);
}


void LogPanel::onGridKeyEvent(wxKeyEvent& event)
{
    int keyCode = event.GetKeyCode();

    if (event.ControlDown())
        switch (keyCode)
        {
            case 'C':
            case WXK_INSERT: //CTRL + C || CTRL + INS
            case WXK_NUMPAD_INSERT:
                copySelectionToClipboard();
                return; // -> swallow event! don't allow default grid commands!
        }

    //else
    //switch (keyCode)
    //{
    //  case WXK_RETURN:
    //  case WXK_NUMPAD_ENTER:
    //      return;
    //}

    event.Skip(); //unknown keypress: propagate
}


void LogPanel::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :)
{
    if (!processingKeyEventHandler_) //avoid recursion
    {
        processingKeyEventHandler_ = true;
        ZEN_ON_SCOPE_EXIT(processingKeyEventHandler_ = false);

        const int keyCode = event.GetKeyCode();

        if (event.ControlDown())
            switch (keyCode)
            {
                case 'A':
                    m_gridMessages->SetFocus();
                    m_gridMessages->selectAllRows(GridEventPolicy::allow);
                    return; // -> swallow event! don't allow default grid commands!
            }
        else
            switch (keyCode)
            {
                //redirect certain (unhandled) keys directly to grid!
                case WXK_UP:
                case WXK_DOWN:
                case WXK_LEFT:
                case WXK_RIGHT:
                case WXK_PAGEUP:
                case WXK_PAGEDOWN:
                case WXK_HOME:
                case WXK_END:

                case WXK_NUMPAD_UP:
                case WXK_NUMPAD_DOWN:
                case WXK_NUMPAD_LEFT:
                case WXK_NUMPAD_RIGHT:
                case WXK_NUMPAD_PAGEUP:
                case WXK_NUMPAD_PAGEDOWN:
                case WXK_NUMPAD_HOME:
                case WXK_NUMPAD_END:
                    if (!isComponentOf(wxWindow::FindFocus(), m_gridMessages) && //don't propagate keyboard commands if grid is already in focus
                        m_gridMessages->IsEnabled())
                    {
                        m_gridMessages->SetFocus();

                        event.SetEventType(wxEVT_KEY_DOWN); //the grid event handler doesn't expect wxEVT_CHAR_HOOK!
                        m_gridMessages->getMainWin().GetEventHandler()->ProcessEvent(event); //propagating event catched at wxTheApp to child leads to recursion, but we prevented it...
                        event.Skip(false); //definitively handled now!
                        return;
                    }
                    break;
            }
    }
    event.Skip();
}


void LogPanel::copySelectionToClipboard()
{
    try
    {
        wxString clipBuf; //perf: old wxString didn't model exponential growth, but now it's std::string-based:
        static_assert(std::is_same_v<wxStringImpl, std::wstring>);

        if (auto prov = m_gridMessages->getDataProvider())
        {
            std::vector<Grid::ColAttributes> colAttr = m_gridMessages->getColumnConfig();
            std::erase_if(colAttr, [](const Grid::ColAttributes& ca) { return !ca.visible; });

            for (size_t row : m_gridMessages->getSelectedRows())
                for (auto it = colAttr.begin(); it != colAttr.end(); ++it)
                {
                    clipBuf += prov->getValue(row, it->type);
                    clipBuf += it == colAttr.end() - 1 ?  L'\n' : L'\t';
                }
        }

        setClipboardText(clipBuf);
    }
    catch (const std::bad_alloc& e)
    {
        showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setMainInstructions(_("Out of memory.") + L"\n\n" + utfTo<std::wstring>(e.what())));
    }
}
