﻿// 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 "PartialDiff.h"
#include "SVN.h"
#include "SVNHelpers.h"
#include "UnicodeUtils.h"
#include "FormatMessageWrapper.h"
#include "SmartHandle.h"
#include "svn_diff.h"
#include "svn_props.h"

// files larger than this are not loaded for a partial commit
static constexpr ULONGLONG MaxFileSize = 10 * 1024 * 1024;

namespace
{
struct DiffBaton
{
    std::vector<CPartialDiff::Hunk>* hunks;
};

svn_error_t* OutputCommon(void*, apr_off_t, apr_off_t, apr_off_t, apr_off_t, apr_off_t, apr_off_t)
{
    return nullptr;
}

svn_error_t* OutputDiffModified(void* baton, apr_off_t origStart, apr_off_t origLen,
                                apr_off_t modStart, apr_off_t modLen, apr_off_t, apr_off_t)
{
    auto*              diffBaton = static_cast<DiffBaton*>(baton);
    CPartialDiff::Hunk hunk;
    hunk.origStart = static_cast<size_t>(origStart);
    hunk.origLen   = static_cast<size_t>(origLen);
    hunk.modStart  = static_cast<size_t>(modStart);
    hunk.modLen    = static_cast<size_t>(modLen);
    hunk.removedSelected.assign(hunk.origLen, true);
    hunk.addedSelected.assign(hunk.modLen, true);
    diffBaton->hunks->push_back(std::move(hunk));
    return nullptr;
}

bool IsValidUtf8(const std::string& buf)
{
    return MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, buf.c_str(), static_cast<int>(buf.size()), nullptr, 0) > 0 || buf.empty();
}
} // namespace

bool CPartialDiff::Load(const CTSVNPath& wcPath)
{
    m_path = wcPath;
    m_error.Empty();
    m_hunks.clear();
    m_baseLines.clear();
    m_wcLines.clear();

    if (!ReadWcFile(m_wc))
        return false;

    SVN                                svn;
    std::map<std::string, std::string> props;
    if (!svn.CatBase(wcPath, m_base, props))
    {
        m_error = svn.GetLastErrorMessage();
        return false;
    }
    if (props.find(SVN_PROP_SPECIAL) != props.end())
    {
        m_error.LoadString(IDS_PARTIALCOMMIT_SPECIAL);
        return false;
    }
    auto mimeType = props.find(SVN_PROP_MIME_TYPE);
    if ((mimeType != props.end()) && svn_mime_type_is_binary(mimeType->second.c_str()))
    {
        m_error.LoadString(IDS_PARTIALCOMMIT_BINARY);
        return false;
    }
    if (!IsTextContent(m_base) || !IsTextContent(m_wc))
    {
        m_error.LoadString(IDS_PARTIALCOMMIT_ENCODING);
        return false;
    }

    SplitLines(m_base, m_baseLines);
    SplitLines(m_wc, m_wcLines);
    m_isUtf8 = IsValidUtf8(m_base) && IsValidUtf8(m_wc);

    // use the line ending of the working copy file if a line without
    // line ending must get one
    m_eol = "\n";
    for (const auto* lines : {&m_wcLines, &m_baseLines})
    {
        const std::string& buf = (lines == &m_wcLines) ? m_wc : m_base;
        auto               it  = std::find_if(lines->begin(), lines->end(), [](const Line& l) { return l.eolLen > 0; });
        if (it != lines->end())
        {
            m_eol = buf.substr(it->start + it->length, it->eolLen);
            break;
        }
    }

    SVNPool                  pool;
    svn_diff_t*              diff     = nullptr;
    svn_diff_file_options_t* options  = svn_diff_file_options_create(pool);
    svn_string_t*            original = svn_string_ncreate(m_base.c_str(), m_base.size(), pool);
    svn_string_t*            modified = svn_string_ncreate(m_wc.c_str(), m_wc.size(), pool);
    svn_error_t*             err      = svn_diff_mem_string_diff(&diff, original, modified, options, pool);
    if (err == nullptr)
    {
        svn_diff_output_fns_t fns = {};
        fns.output_common         = OutputCommon;
        fns.output_diff_modified  = OutputDiffModified;
        DiffBaton baton{&m_hunks};
        err = svn_diff_output2(diff, &baton, &fns, nullptr, nullptr);
    }
    if (err)
    {
        m_error = CUnicodeUtils::GetUnicode(err->message ? err->message : "");
        svn_error_clear(err);
        m_hunks.clear();
        return false;
    }
    if (m_hunks.empty())
    {
        m_error.LoadString(IDS_PARTIALCOMMIT_NOCHANGES);
        return false;
    }
    return true;
}

bool CPartialDiff::ReadWcFile(std::string& content)
{
    WIN32_FILE_ATTRIBUTE_DATA attribs = {};
    if (!GetFileAttributesEx(m_path.GetWinPath(), GetFileExInfoStandard, &attribs))
    {
        m_error = static_cast<LPCWSTR>(CFormatMessageWrapper());
        return false;
    }
    m_wcSize = (static_cast<ULONGLONG>(attribs.nFileSizeHigh) << 32) | attribs.nFileSizeLow;
    m_wcTime = attribs.ftLastWriteTime;
    if (m_wcSize > MaxFileSize)
    {
        m_error.LoadString(IDS_PARTIALCOMMIT_TOOLARGE);
        return false;
    }

    CAutoFile hFile = CreateFile(m_path.GetWinPath(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
    if (!hFile)
    {
        m_error = static_cast<LPCWSTR>(CFormatMessageWrapper());
        return false;
    }
    content.resize(static_cast<size_t>(m_wcSize));
    DWORD read = 0;
    if (!content.empty() && (!ReadFile(hFile, content.data(), static_cast<DWORD>(content.size()), &read, nullptr) || (read != content.size())))
    {
        m_error = static_cast<LPCWSTR>(CFormatMessageWrapper());
        return false;
    }
    return true;
}

bool CPartialDiff::IsWcUnchanged() const
{
    WIN32_FILE_ATTRIBUTE_DATA attribs = {};
    if (!GetFileAttributesEx(m_path.GetWinPath(), GetFileExInfoStandard, &attribs))
        return false;
    ULONGLONG size = (static_cast<ULONGLONG>(attribs.nFileSizeHigh) << 32) | attribs.nFileSizeLow;
    return (size == m_wcSize) && (CompareFileTime(&attribs.ftLastWriteTime, &m_wcTime) == 0);
}

void CPartialDiff::SplitLines(const std::string& buf, std::vector<Line>& lines)
{
    // the same rule as the svn diff library: a line ends with CRLF, CR or LF
    lines.clear();
    size_t start = 0;
    size_t len   = buf.size();
    for (size_t i = 0; i < len; ++i)
    {
        if ((buf[i] != '\r') && (buf[i] != '\n'))
            continue;
        size_t eolLen = ((buf[i] == '\r') && (i + 1 < len) && (buf[i + 1] == '\n')) ? 2 : 1;
        lines.push_back({start, i - start, eolLen});
        i += eolLen - 1;
        start = i + 1;
    }
    if (start < len)
        lines.push_back({start, len - start, 0});
}

bool CPartialDiff::IsTextContent(const std::string& buf)
{
    // UTF-16 and UTF-32 files have a BOM or NUL bytes. The line split
    // of the svn diff library does not work for these encodings.
    if ((buf.size() >= 2) && ((static_cast<unsigned char>(buf[0]) == 0xFF && static_cast<unsigned char>(buf[1]) == 0xFE) ||
                              (static_cast<unsigned char>(buf[0]) == 0xFE && static_cast<unsigned char>(buf[1]) == 0xFF)))
        return false;
    return buf.find('\0') == std::string::npos;
}

std::string CPartialDiff::ToUtf8(const std::string& buf, const Line& line) const
{
    std::string text = buf.substr(line.start, line.length);
    if ((line.start == 0) && (text.size() >= 3) && (text.compare(0, 3, "\xEF\xBB\xBF") == 0))
        text.erase(0, 3);
    if (m_isUtf8 || text.empty())
        return text;
    // the file is not valid UTF-8: show it in the ANSI code page
    int          wideLen = MultiByteToWideChar(CP_ACP, 0, text.c_str(), static_cast<int>(text.size()), nullptr, 0);
    std::wstring wide(wideLen, L'\0');
    MultiByteToWideChar(CP_ACP, 0, text.c_str(), static_cast<int>(text.size()), wide.data(), wideLen);
    return CUnicodeUtils::StdGetUTF8(wide);
}

std::string CPartialDiff::GetBaseLineUtf8(size_t line) const
{
    return ToUtf8(m_base, m_baseLines[line]);
}

std::string CPartialDiff::GetWcLineUtf8(size_t line) const
{
    return ToUtf8(m_wc, m_wcLines[line]);
}

void CPartialDiff::SetRemovedSelected(size_t hunk, size_t index, bool selected)
{
    m_hunks[hunk].removedSelected[index] = selected;
}

void CPartialDiff::SetAddedSelected(size_t hunk, size_t index, bool selected)
{
    m_hunks[hunk].addedSelected[index] = selected;
}

void CPartialDiff::SelectHunk(size_t hunk, bool selected)
{
    auto& h = m_hunks[hunk];
    std::fill(h.removedSelected.begin(), h.removedSelected.end(), selected);
    std::fill(h.addedSelected.begin(), h.addedSelected.end(), selected);
}

void CPartialDiff::SelectAll(bool selected)
{
    for (size_t i = 0; i < m_hunks.size(); ++i)
        SelectHunk(i, selected);
}

void CPartialDiff::CountSelected(size_t& selected, size_t& total) const
{
    selected = 0;
    total    = 0;
    for (const auto& h : m_hunks)
    {
        total += h.removedSelected.size() + h.addedSelected.size();
        selected += static_cast<size_t>(std::count(h.removedSelected.begin(), h.removedSelected.end(), true));
        selected += static_cast<size_t>(std::count(h.addedSelected.begin(), h.addedSelected.end(), true));
    }
}

bool CPartialDiff::IsAllSelected() const
{
    size_t selected = 0;
    size_t total    = 0;
    CountSelected(selected, total);
    return selected == total;
}

bool CPartialDiff::IsNoneSelected() const
{
    size_t selected = 0;
    size_t total    = 0;
    CountSelected(selected, total);
    return selected == 0;
}

int CPartialDiff::GetHunkState(size_t hunk) const
{
    const auto& h        = m_hunks[hunk];
    size_t      total    = h.removedSelected.size() + h.addedSelected.size();
    size_t      selected = static_cast<size_t>(std::count(h.removedSelected.begin(), h.removedSelected.end(), true) +
                                                std::count(h.addedSelected.begin(), h.addedSelected.end(), true));
    if (selected == total)
        return 1;
    if (selected == 0)
        return 0;
    return -1;
}

std::string CPartialDiff::BuildPartial() const
{
    std::string out;
    out.reserve(std::max(m_base.size(), m_wc.size()));
    bool missingEol = false;
    auto append     = [&](const std::string& buf, const Line& line) {
        // a line without line ending can only be the last line of a file.
        // If other lines follow it in the result, it needs a line ending.
        if (missingEol)
            out += m_eol;
        out.append(buf, line.start, line.length + line.eolLen);
        missingEol = (line.eolLen == 0);
    };

    size_t basePos = 0;
    for (const auto& h : m_hunks)
    {
        for (; basePos < h.origStart; ++basePos)
            append(m_base, m_baseLines[basePos]);
        for (size_t i = 0; i < h.origLen; ++i)
        {
            // a removed line which is not selected stays in the file
            if (!h.removedSelected[i])
                append(m_base, m_baseLines[h.origStart + i]);
        }
        for (size_t i = 0; i < h.modLen; ++i)
        {
            if (h.addedSelected[i])
                append(m_wc, m_wcLines[h.modStart + i]);
        }
        basePos = h.origStart + h.origLen;
    }
    for (; basePos < m_baseLines.size(); ++basePos)
        append(m_base, m_baseLines[basePos]);
    return out;
}
