﻿// TortoiseSVN - a Windows shell extension for easy version control

// Copyright (C) 2026 - TortoiseSVN

// This program 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 2
// of the License, or (at your option) any later version.

// This program 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 this program; if not, write to the Free Software Foundation,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
#include "stdafx.h"
#include "resource.h"
#include "DiffSelectCtrl.h"
#include "PartialDiff.h"
#include "registry.h"
#include "UnicodeUtils.h"
#include "Theme.h"
#include "DPIAware.h"
#include "../TortoiseUDiff/UDiffColors.h"
#include <algorithm>
#include <cmath>

// number of unchanged lines shown before and after a change
static constexpr size_t ContextLines = 3;

// styles
static constexpr int StyleContext     = 1;
static constexpr int StyleAdded       = 2;
static constexpr int StyleRemoved     = 3;
static constexpr int StyleHeader      = 4;
static constexpr int StyleAddedOff    = 5;
static constexpr int StyleRemovedOff  = 6;
static constexpr int StyleMessage     = 7;

// markers
static constexpr int MarkerChecked    = 0;
static constexpr int MarkerUnchecked  = 1;
static constexpr int MarkerPartial    = 2;
static constexpr int MarkerMask       = (1 << MarkerChecked) | (1 << MarkerUnchecked) | (1 << MarkerPartial);
static constexpr int CheckMargin      = 1;

// context menu commands
static constexpr UINT CmdToggle       = 1;
static constexpr UINT CmdSelectHunk   = 2;
static constexpr UINT CmdDeselectHunk = 3;
static constexpr UINT CmdSelectAll    = 4;
static constexpr UINT CmdDeselectAll  = 5;

const UINT CDiffSelectCtrl::WM_DIFFSELCHANGED = RegisterWindowMessage(L"TORTOISESVN_DIFFSELCHANGED_MSG");

IMPLEMENT_DYNAMIC(CDiffSelectCtrl, CWnd)

BEGIN_MESSAGE_MAP(CDiffSelectCtrl, CWnd)
    ON_WM_KEYDOWN()
    ON_WM_CONTEXTMENU()
END_MESSAGE_MAP()

CDiffSelectCtrl::CDiffSelectCtrl()
    : m_directFunction(0)
    , m_directPointer(0)
    , m_themeCallbackId(0)
    , m_diff(nullptr)
{
}

CDiffSelectCtrl::~CDiffSelectCtrl()
{
    CTheme::Instance().RemoveRegisteredCallback(m_themeCallbackId);
}

LRESULT CDiffSelectCtrl::Call(UINT message, WPARAM wParam, LPARAM lParam) const
{
    ASSERT(::IsWindow(m_hWnd));
    ASSERT(m_directFunction);
    return reinterpret_cast<SciFnDirect>(m_directFunction)(m_directPointer, message, wParam, lParam);
}

void CDiffSelectCtrl::Init()
{
    m_directFunction = SendMessage(SCI_GETDIRECTFUNCTION, 0, 0);
    m_directPointer  = SendMessage(SCI_GETDIRECTPOINTER, 0, 0);
    Call(SCI_SETILEXER, 0, reinterpret_cast<sptr_t>(nullptr));
    Call(SCI_SETCODEPAGE, SC_CP_UTF8);
    Call(SCI_USEPOPUP, SC_POPUP_NEVER);
    Call(SCI_SETUNDOCOLLECTION, 0);
    Call(SCI_SETMARGINWIDTHN, 0, 0);
    Call(SCI_SETMARGINTYPEN, CheckMargin, SC_MARGIN_SYMBOL);
    Call(SCI_SETMARGINMASKN, CheckMargin, MarkerMask);
    Call(SCI_SETMARGINSENSITIVEN, CheckMargin, 1);
    Call(SCI_SETMARGINCURSORN, CheckMargin, SC_CURSORARROW);
    Call(SCI_SETCARETLINEVISIBLE, 1);
    Call(SCI_SETCARETLINEVISIBLEALWAYS, 1);
    Call(SCI_SETSCROLLWIDTHTRACKING, 1);
    Call(SCI_SETSCROLLWIDTH, 1);
    Call(SCI_SETREADONLY, 1);
    SetupStyles();
    DefineMarkers();

    m_themeCallbackId = CTheme::Instance().RegisterThemeChangeCallback(
        [this]() {
            SetupStyles();
            DefineMarkers();
            UpdateAllLineStates();
        });
}

void CDiffSelectCtrl::SetupStyles()
{
    bool        dark      = CTheme::Instance().IsDarkTheme();
    COLORREF    textColor = dark ? UDiffTextColorDark : ::GetSysColor(COLOR_WINDOWTEXT);
    COLORREF    backColor = dark ? UDiffBackColorDark : ::GetSysColor(COLOR_WINDOW);
    std::string fontName  = CUnicodeUtils::StdGetUTF8(static_cast<std::wstring>(CRegStdString(L"Software\\TortoiseSVN\\UDiffFontName", L"Consolas")));
    int         fontSize  = static_cast<int>(static_cast<DWORD>(CRegStdDWORD(L"Software\\TortoiseSVN\\UDiffFontSize", 10)));

    Call(SCI_STYLESETFORE, STYLE_DEFAULT, textColor);
    Call(SCI_STYLESETBACK, STYLE_DEFAULT, backColor);
    Call(SCI_STYLESETFONT, STYLE_DEFAULT, reinterpret_cast<LPARAM>(fontName.c_str()));
    Call(SCI_STYLESETSIZE, STYLE_DEFAULT, fontSize);
    Call(SCI_STYLECLEARALL);
    Call(SCI_SETCARETFORE, textColor);
    Call(SCI_SETCARETLINEBACK, dark ? RGB(0x30, 0x30, 0x30) : RGB(0xF0, 0xF0, 0xF0));
    Call(SCI_SETSELBACK, TRUE, ::GetSysColor(COLOR_HIGHLIGHT));
    Call(SCI_SETSELFORE, TRUE, ::GetSysColor(COLOR_HIGHLIGHTTEXT));
    Call(SCI_SETMARGINBACKN, CheckMargin, dark ? RGB(0x2B, 0x2B, 0x2B) : RGB(0xF0, 0xF0, 0xF0));
    Call(SCI_SETFOLDMARGINCOLOUR, TRUE, dark ? RGB(0x2B, 0x2B, 0x2B) : RGB(0xF0, 0xF0, 0xF0));
    Call(SCI_SETFOLDMARGINHICOLOUR, TRUE, dark ? RGB(0x2B, 0x2B, 0x2B) : RGB(0xF0, 0xF0, 0xF0));

    auto setStyle = [this](int style, COLORREF fore, COLORREF back) {
        Call(SCI_STYLESETFORE, style, fore);
        Call(SCI_STYLESETBACK, style, back);
        Call(SCI_STYLESETEOLFILLED, style, 1);
    };
    COLORREF offColor = dark ? RGB(0x80, 0x80, 0x80) : RGB(0xA0, 0xA0, 0xA0);
    setStyle(StyleContext, textColor, backColor);
    setStyle(StyleAdded, dark ? UDIFF_COLORFOREADDED_DARK : UDIFF_COLORFOREADDED, dark ? UDIFF_COLORBACKADDED_DARK : UDIFF_COLORBACKADDED);
    setStyle(StyleRemoved, dark ? UDIFF_COLORFOREREMOVED_DARK : UDIFF_COLORFOREREMOVED, dark ? UDIFF_COLORBACKREMOVED_DARK : UDIFF_COLORBACKREMOVED);
    setStyle(StyleHeader, dark ? UDIFF_COLORFOREPOSITION_DARK : UDIFF_COLORFOREPOSITION, dark ? RGB(0x28, 0x28, 0x38) : RGB(0xE8, 0xE8, 0xF8));
    setStyle(StyleAddedOff, offColor, backColor);
    setStyle(StyleRemovedOff, offColor, backColor);
    setStyle(StyleMessage, offColor, backColor);
    Call(SCI_STYLESETITALIC, StyleMessage, 1);
    Call(SCI_STYLESETITALIC, StyleAddedOff, 1);
    Call(SCI_STYLESETITALIC, StyleRemovedOff, 1);
}

void CDiffSelectCtrl::DefineMarkers()
{
    // draw the check box images with RGBA pixels
    int  size   = CDPIAware::Instance().Scale(GetSafeHwnd(), 13);
    bool dark   = CTheme::Instance().IsDarkTheme();
    auto border = dark ? RGB(0xC0, 0xC0, 0xC0) : RGB(0x33, 0x33, 0x33);
    auto inner  = dark ? RGB(0x20, 0x20, 0x20) : RGB(0xFF, 0xFF, 0xFF);
    auto accent = dark ? RGB(0x4C, 0xA0, 0xE0) : RGB(0x00, 0x78, 0xD7);
    auto white  = RGB(0xFF, 0xFF, 0xFF);

    auto distToSegment = [](float px, float py, float ax, float ay, float bx, float by) {
        float dx = bx - ax;
        float dy = by - ay;
        float t  = ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy);
        t        = std::clamp(t, 0.0f, 1.0f);
        float cx = ax + t * dx - px;
        float cy = ay + t * dy - py;
        return std::sqrt(cx * cx + cy * cy);
    };

    for (int marker : {MarkerChecked, MarkerUnchecked, MarkerPartial})
    {
        std::vector<BYTE> pixels(static_cast<size_t>(size) * size * 4);
        float             s         = static_cast<float>(size);
        float             thickness = std::max(1.5f, s / 7.0f);
        for (int y = 0; y < size; ++y)
        {
            for (int x = 0; x < size; ++x)
            {
                COLORREF color   = inner;
                bool     isFrame = (x == 0) || (y == 0) || (x == size - 1) || (y == size - 1);
                if (isFrame)
                    color = (marker == MarkerChecked) ? accent : border;
                else if (marker == MarkerChecked)
                {
                    float px = x + 0.5f;
                    float py = y + 0.5f;
                    float d  = std::min(distToSegment(px, py, s * 0.22f, s * 0.52f, s * 0.42f, s * 0.72f),
                                        distToSegment(px, py, s * 0.42f, s * 0.72f, s * 0.78f, s * 0.30f));
                    color    = (d <= thickness / 2.0f) ? white : accent;
                }
                else if (marker == MarkerPartial)
                {
                    int m = size / 4 + 1;
                    if ((x >= m) && (y >= m) && (x < size - m) && (y < size - m))
                        color = accent;
                }
                size_t pos      = (static_cast<size_t>(y) * size + x) * 4;
                pixels[pos + 0] = GetRValue(color);
                pixels[pos + 1] = GetGValue(color);
                pixels[pos + 2] = GetBValue(color);
                pixels[pos + 3] = 0xFF;
            }
        }
        Call(SCI_RGBAIMAGESETWIDTH, size);
        Call(SCI_RGBAIMAGESETHEIGHT, size);
        Call(SCI_MARKERDEFINERGBAIMAGE, marker, reinterpret_cast<LPARAM>(pixels.data()));
    }
    Call(SCI_SETMARGINWIDTHN, CheckMargin, size + CDPIAware::Instance().Scale(GetSafeHwnd(), 8));
}

void CDiffSelectCtrl::SetText(const std::string& text)
{
    Call(SCI_SETREADONLY, 0);
    Call(SCI_SETTEXT, 0, reinterpret_cast<LPARAM>(text.c_str()));
    Call(SCI_SETREADONLY, 1);
    Call(SCI_GOTOPOS, 0);
    Call(SCI_SETSCROLLWIDTH, 1);
}

void CDiffSelectCtrl::SetMessage(const CString& message)
{
    m_diff = nullptr;
    m_lines.clear();
    Call(SCI_MARKERDELETEALL, static_cast<WPARAM>(-1));
    std::string text = CUnicodeUtils::StdGetUTF8(static_cast<LPCWSTR>(message));
    SetText(text);
    Call(SCI_STARTSTYLING, 0);
    Call(SCI_SETSTYLING, text.size(), StyleMessage);
}

void CDiffSelectCtrl::SetDiff(CPartialDiff* diff)
{
    m_diff = diff;
    m_lines.clear();
    Call(SCI_MARKERDELETEALL, static_cast<WPARAM>(-1));
    if (m_diff == nullptr)
    {
        SetText(std::string());
        return;
    }

    // build the text of the diff and remember which line shows what
    std::string text;
    const auto& hunks   = m_diff->GetHunks();
    size_t      shownTo = 0; // first BASE line which is not shown yet
    auto        addLine = [&](LineType type, size_t hunk, size_t index, char prefix, const std::string& content) {
        if (!m_lines.empty())
            text += '\n';
        text += prefix;
        text += content;
        m_lines.push_back({type, hunk, index});
    };
    for (size_t h = 0; h < hunks.size(); ++h)
    {
        const auto& hunk         = hunks[h];
        size_t      contextStart = std::max(shownTo, hunk.origStart > ContextLines ? hunk.origStart - ContextLines : 0);
        size_t      nextStart    = (h + 1 < hunks.size()) ? hunks[h + 1].origStart : m_diff->GetBaseLineCount();
        size_t      contextEnd   = std::min(hunk.origStart + hunk.origLen + ContextLines, nextStart);

        CStringA header;
        header.Format("@@ -%Iu,%Iu +%Iu,%Iu @@", hunk.origStart + 1, hunk.origLen, hunk.modStart + 1, hunk.modLen);
        addLine(LineType::Header, h, 0, ' ', static_cast<LPCSTR>(header));
        for (size_t l = contextStart; l < hunk.origStart; ++l)
            addLine(LineType::Context, h, 0, ' ', m_diff->GetBaseLineUtf8(l));
        for (size_t i = 0; i < hunk.origLen; ++i)
            addLine(LineType::Removed, h, i, '-', m_diff->GetBaseLineUtf8(hunk.origStart + i));
        for (size_t i = 0; i < hunk.modLen; ++i)
            addLine(LineType::Added, h, i, '+', m_diff->GetWcLineUtf8(hunk.modStart + i));
        for (size_t l = hunk.origStart + hunk.origLen; l < contextEnd; ++l)
            addLine(LineType::Context, h, 0, ' ', m_diff->GetBaseLineUtf8(l));
        shownTo = contextEnd;
    }
    SetText(text);
    UpdateAllLineStates();
}

bool CDiffSelectCtrl::IsLineSelected(const DisplayLine& line) const
{
    const auto& hunk = m_diff->GetHunks()[line.hunk];
    if (line.type == LineType::Removed)
        return hunk.removedSelected[line.index];
    if (line.type == LineType::Added)
        return hunk.addedSelected[line.index];
    return false;
}

void CDiffSelectCtrl::UpdateLineState(size_t displayLine)
{
    const auto& line = m_lines[displayLine];
    int         style  = StyleContext;
    int         marker = -1;
    switch (line.type)
    {
        case LineType::Header:
        {
            style     = StyleHeader;
            int state = m_diff->GetHunkState(line.hunk);
            marker    = (state == 1) ? MarkerChecked : ((state == 0) ? MarkerUnchecked : MarkerPartial);
        }
        break;
        case LineType::Removed:
        case LineType::Added:
        {
            bool selected = IsLineSelected(line);
            marker        = selected ? MarkerChecked : MarkerUnchecked;
            if (line.type == LineType::Removed)
                style = selected ? StyleRemoved : StyleRemovedOff;
            else
                style = selected ? StyleAdded : StyleAddedOff;
        }
        break;
        case LineType::Context:
            break;
    }
    Call(SCI_MARKERDELETE, displayLine, static_cast<LPARAM>(-1));
    if (marker >= 0)
        Call(SCI_MARKERADD, displayLine, marker);
    auto start = Call(SCI_POSITIONFROMLINE, displayLine);
    auto len   = Call(SCI_LINELENGTH, displayLine);
    Call(SCI_STARTSTYLING, start);
    Call(SCI_SETSTYLING, len, style);
}

void CDiffSelectCtrl::UpdateAllLineStates()
{
    if (m_diff == nullptr)
        return;
    for (size_t i = 0; i < m_lines.size(); ++i)
        UpdateLineState(i);
}

size_t CDiffSelectCtrl::GetHunkOfLine(size_t displayLine) const
{
    return m_lines[displayLine].hunk;
}

void CDiffSelectCtrl::SelectLines(size_t firstLine, size_t lastLine, bool select)
{
    for (size_t l = firstLine; l <= lastLine && l < m_lines.size(); ++l)
    {
        const auto& line = m_lines[l];
        if (line.type == LineType::Header)
            m_diff->SelectHunk(line.hunk, select);
        else if (line.type == LineType::Removed)
            m_diff->SetRemovedSelected(line.hunk, line.index, select);
        else if (line.type == LineType::Added)
            m_diff->SetAddedSelected(line.hunk, line.index, select);
    }
    UpdateAllLineStates();
    NotifyParent();
}

void CDiffSelectCtrl::ToggleLines(size_t firstLine, size_t lastLine)
{
    if ((m_diff == nullptr) || m_lines.empty())
        return;
    lastLine = std::min(lastLine, m_lines.size() - 1);
    // if all changed lines in the range are selected, deselect them.
    // Otherwise select all of them.
    bool allSelected = true;
    bool anyChange   = false;
    for (size_t l = firstLine; l <= lastLine; ++l)
    {
        const auto& line = m_lines[l];
        if (line.type == LineType::Header)
        {
            anyChange = true;
            if (m_diff->GetHunkState(line.hunk) != 1)
                allSelected = false;
        }
        else if (line.type != LineType::Context)
        {
            anyChange = true;
            if (!IsLineSelected(line))
                allSelected = false;
        }
    }
    if (anyChange)
        SelectLines(firstLine, lastLine, !allSelected);
}

void CDiffSelectCtrl::NotifyParent() const
{
    GetParent()->SendMessage(WM_DIFFSELCHANGED, 0, reinterpret_cast<LPARAM>(m_hWnd));
}

BOOL CDiffSelectCtrl::OnChildNotify(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pLResult)
{
    if (message == WM_NOTIFY)
    {
        auto* lpNmHdr = reinterpret_cast<NMHDR*>(lParam);
        if ((lpNmHdr->hwndFrom == m_hWnd) && (lpNmHdr->code == SCN_MARGINCLICK))
        {
            auto* scn  = reinterpret_cast<SCNotification*>(lParam);
            auto  line = static_cast<size_t>(Call(SCI_LINEFROMPOSITION, scn->position));
            ToggleLines(line, line);
            return TRUE;
        }
    }
    return CWnd::OnChildNotify(message, wParam, lParam, pLResult);
}

void CDiffSelectCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
    if ((nChar == VK_SPACE) && (m_diff != nullptr))
    {
        auto selStart  = Call(SCI_GETSELECTIONSTART);
        auto selEnd    = Call(SCI_GETSELECTIONEND);
        auto firstLine = static_cast<size_t>(Call(SCI_LINEFROMPOSITION, selStart));
        auto lastLine  = static_cast<size_t>(Call(SCI_LINEFROMPOSITION, selEnd));
        // a selection which ends at the start of a line does not include that line
        if ((lastLine > firstLine) && (Call(SCI_POSITIONFROMLINE, lastLine) == selEnd))
            --lastLine;
        ToggleLines(firstLine, lastLine);
        return;
    }
    CWnd::OnKeyDown(nChar, nRepCnt, nFlags);
}

void CDiffSelectCtrl::OnContextMenu(CWnd* /*pWnd*/, CPoint point)
{
    if ((m_diff == nullptr) || m_lines.empty())
        return;

    size_t line = 0;
    if ((point.x == -1) && (point.y == -1))
    {
        auto pos = Call(SCI_GETCURRENTPOS);
        line     = static_cast<size_t>(Call(SCI_LINEFROMPOSITION, pos));
        point.x  = static_cast<int>(Call(SCI_POINTXFROMPOSITION, 0, pos));
        point.y  = static_cast<int>(Call(SCI_POINTYFROMPOSITION, 0, pos));
        ClientToScreen(&point);
    }
    else
    {
        CPoint clientPoint = point;
        ScreenToClient(&clientPoint);
        auto firstVisible = Call(SCI_GETFIRSTVISIBLELINE);
        auto lineHeight   = Call(SCI_TEXTHEIGHT, 0);
        line              = static_cast<size_t>(firstVisible + (lineHeight ? clientPoint.y / lineHeight : 0));
    }
    line = std::min(line, m_lines.size() - 1);

    auto selStart  = Call(SCI_GETSELECTIONSTART);
    auto selEnd    = Call(SCI_GETSELECTIONEND);
    auto firstLine = static_cast<size_t>(Call(SCI_LINEFROMPOSITION, selStart));
    auto lastLine  = static_cast<size_t>(Call(SCI_LINEFROMPOSITION, selEnd));
    if ((lastLine > firstLine) && (Call(SCI_POSITIONFROMLINE, lastLine) == selEnd))
        --lastLine;
    // use the line below the mouse if it is outside of the text selection
    if ((line < firstLine) || (line > lastLine))
        firstLine = lastLine = line;

    size_t hunk  = GetHunkOfLine(line);
    int    state = m_diff->GetHunkState(hunk);

    CMenu popup;
    if (!popup.CreatePopupMenu())
        return;
    popup.AppendMenu(MF_STRING, CmdToggle, CString(MAKEINTRESOURCE(IDS_PARTIALCOMMIT_TOGGLE)));
    popup.AppendMenu(MF_SEPARATOR);
    popup.AppendMenu(MF_STRING | (state == 1 ? MF_GRAYED : 0), CmdSelectHunk, CString(MAKEINTRESOURCE(IDS_PARTIALCOMMIT_SELECTHUNK)));
    popup.AppendMenu(MF_STRING | (state == 0 ? MF_GRAYED : 0), CmdDeselectHunk, CString(MAKEINTRESOURCE(IDS_PARTIALCOMMIT_DESELECTHUNK)));
    popup.AppendMenu(MF_SEPARATOR);
    popup.AppendMenu(MF_STRING, CmdSelectAll, CString(MAKEINTRESOURCE(IDS_PARTIALCOMMIT_SELECTALL)));
    popup.AppendMenu(MF_STRING, CmdDeselectAll, CString(MAKEINTRESOURCE(IDS_PARTIALCOMMIT_DESELECTALL)));

    UINT cmd = popup.TrackPopupMenu(TPM_LEFTALIGN | TPM_RETURNCMD | TPM_RIGHTBUTTON, point.x, point.y, this);
    switch (cmd)
    {
        case CmdToggle:
            ToggleLines(firstLine, lastLine);
            break;
        case CmdSelectHunk:
        case CmdDeselectHunk:
            m_diff->SelectHunk(hunk, cmd == CmdSelectHunk);
            UpdateAllLineStates();
            NotifyParent();
            break;
        case CmdSelectAll:
        case CmdDeselectAll:
            m_diff->SelectAll(cmd == CmdSelectAll);
            UpdateAllLineStates();
            NotifyParent();
            break;
        default:
            break;
    }
}
