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

// Copyright (C) 2012-2013, 2018 - TortoiseGit
// Copyright (C) 2003-2008, 2011-2016, 2021, 2024 - 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 <Shlwapi.h>
#include <fstream>
#include "codecvt.h"
#include "ResModule.h"
#include "POFile.h"

#include "../Utils/UnicodeUtils.h"

#include <algorithm>
#include <memory>
#include <functional>

#define MYERROR          \
    {                    \
        CUtils::Error(); \
        return FALSE;    \
    }

CPOFile::CPOFile()
    : m_bQuiet(false)
    , m_bAdjustEOLs(false)
{
}

CPOFile::~CPOFile()
{
}

static bool StartsWith(const wchar_t* heystacl, const wchar_t* needle)
{
    return wcsncmp(heystacl, needle, wcslen(needle)) == 0;
}

BOOL CPOFile::ParseFile(LPCWSTR szPath, BOOL bUpdateExisting, bool bAdjustEOLs)
{
    if (!PathFileExists(szPath))
        return FALSE;

    m_bAdjustEOLs = bAdjustEOLs;

    if (!m_bQuiet)
        fwprintf(stdout, L"parsing file %s...\n", szPath);

    int nEntries    = 0;
    int nDeleted    = 0;
    int nTranslated = 0;
    //since stream classes still expect the filepath in char and not wchar_t
    //we need to convert the filepath to multibyte
    char filepath[MAX_PATH + 1] = {0};
    WideCharToMultiByte(CP_ACP, 0, szPath, -1, filepath, _countof(filepath) - 1, nullptr, nullptr);

    std::ifstream file;
    file.open(filepath);
    if (!file.good())
    {
        fwprintf(stderr, L"can't open input file %s\n", szPath);
        return FALSE;
    }
    auto                      line = std::make_unique<char[]>(2 * MAX_STRING_LENGTH);
    std::vector<std::wstring> entry;
    do
    {
        file.getline(line.get(), 2 * MAX_STRING_LENGTH);
        if (line.get()[0] == 0)
        {
            //empty line means end of entry!
            TagResourceEntry resEntry = {0};
            std::wstring  msgid;
            std::wstring  regexSearch, regexReplace;
            int           type = 0;
            for (auto I = entry.cbegin(); I != entry.cend(); ++I)
            {
                if (StartsWith(I->c_str(), L"# "))
                {
                    //user comment
                    if (StartsWith(I->c_str(), L"# regexsearch="))
                        regexSearch = I->substr(14);
                    else if (StartsWith(I->c_str(), L"# regexreplace="))
                        regexReplace = I->substr(15);
                    else
                        resEntry.translatorComments.push_back(I->c_str());
                    if (!regexSearch.empty() && !regexReplace.empty())
                    {
                        m_regexes.push_back(std::make_tuple(regexSearch, regexReplace));
                        regexSearch.clear();
                        regexReplace.clear();
                    }
                    type = 0;
                }
                if (StartsWith(I->c_str(), L"#."))
                {
                    //automatic comments
                    resEntry.automaticComments.push_back(I->c_str());
                    type = 0;
                }
                if (StartsWith(I->c_str(), L"#,"))
                {
                    //flag
                    resEntry.flag = I->c_str();
                    type          = 0;
                }
                if (StartsWith(I->c_str(), L"msgid"))
                {
                    //message id
                    msgid = I->c_str();
                    msgid = std::wstring(msgid.substr(7, msgid.size() - 8));

                    std::wstring s = msgid;
                    s.erase(s.cbegin(), std::find_if(s.cbegin(), s.cend(), [](const auto& c) { return !iswspace(c); }));
                    if (s.size())
                        nEntries++;
                    type = 1;
                }
                if (StartsWith(I->c_str(), L"msgstr"))
                {
                    //message string
                    resEntry.msgStr = I->c_str();
                    resEntry.msgStr = resEntry.msgStr.substr(8, resEntry.msgStr.length() - 9);
                    if (!resEntry.msgStr.empty())
                        nTranslated++;
                    type = 2;
                }
                if (StartsWith(I->c_str(), L"\""))
                {
                    if (type == 1)
                    {
                        std::wstring temp = I->c_str();
                        temp              = temp.substr(1, temp.length() - 2);
                        msgid += temp;
                    }
                    if (type == 2)
                    {
                        if (resEntry.msgStr.empty())
                            nTranslated++;
                        std::wstring temp = I->c_str();
                        temp              = temp.substr(1, temp.length() - 2);
                        resEntry.msgStr += temp;
                    }
                }
            }
            entry.clear();
            if ((bUpdateExisting) && (this->count(msgid) == 0))
                nDeleted++;
            else
            {
                if ((m_bAdjustEOLs) && (msgid.find(L"\\r\\n") != std::string::npos))
                {
                    AdjustEOLs(resEntry.msgStr);
                }
                // always use the new data for generated comments/flags
                auto newEntry              = (*this)[msgid];
                resEntry.automaticComments = newEntry.automaticComments;
                resEntry.flag              = newEntry.flag;
                resEntry.resourceIDs       = newEntry.resourceIDs;

                (*this)[msgid] = resEntry;
            }
            msgid.clear();
        }
        else
        {
            entry.push_back(CUnicodeUtils::StdGetUnicode(line.get()));
        }
    } while (file.gcount() > 0);
    printf("%s", file.getloc().name().c_str());
    file.close();
    TagResourceEntry emptyEntry = {0};
    (*this)[std::wstring(L"")] = emptyEntry;
    if (!m_bQuiet)
        fwprintf(stdout, L"%d Entries found, %d were already translated and %d got deleted\n", nEntries, nTranslated, nDeleted);
    return TRUE;
}

BOOL CPOFile::SaveFile(LPCWSTR szPath, LPCWSTR lpszHeaderFile)
{
    //since stream classes still expect the filepath in char and not wchar_t
    //we need to convert the filepath to multibyte
    char filepath[MAX_PATH + 1] = {0};
    int  nEntries               = 0;
    WideCharToMultiByte(CP_ACP, 0, szPath, -1, filepath, _countof(filepath) - 1, nullptr, nullptr);

    std::ofstream file;
    file.open(filepath, std::ios_base::binary);

    if ((lpszHeaderFile) && (lpszHeaderFile[0]) && (PathFileExists(lpszHeaderFile)))
    {
        // read the header file and save it to the top of the pot file
        std::ifstream inFile;
        inFile.open(lpszHeaderFile, std::ios_base::binary);

        char ch;
        while (inFile && inFile.get(ch))
            file.put(ch);
        inFile.close();
    }
    else
    {
        file << "# SOME DESCRIPTIVE TITLE.\n";
        file << "# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n";
        file << "# This file is distributed under the same license as the PACKAGE package.\n";
        file << "# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.\n";
        file << "#\n";
        file << "#, fuzzy\n";
        file << "msgid \"\"\n";
        file << "msgstr \"\"\n";
        file << "\"Project-Id-Version: PACKAGE VERSION\\n\"\n";
        file << "\"Report-Msgid-Bugs-To: \\n\"\n";
        file << "\"POT-Creation-Date: 1900-01-01 00:00+0000\\n\"\n";
        file << "\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n";
        file << "\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n";
        file << "\"Language-Team: LANGUAGE <LL@li.org>\\n\"\n";
        file << "\"MIME-Version: 1.0\\n\"\n";
        file << "\"Content-Type: text/plain; charset=UTF-8\\n\"\n";
        file << "\"Content-Transfer-Encoding: 8bit\\n\"\n\n";
    }
    file << "\n";
    file << "# msgid/msgstr fields for Accelerator keys\n";
    file << "# Format is: \"ID:xxxxxx:VACS+X\" where:\n";
    file << "#    ID:xxxxx = the menu ID corresponding to the accelerator\n";
    file << "#    V = Virtual key (or blank if not used) - nearly always set!\n";
    file << "#    A = Alt key     (or blank if not used)\n";
    file << "#    C = Ctrl key    (or blank if not used)\n";
    file << "#    S = Shift key   (or blank if not used)\n";
    file << "#    X = upper case character\n";
    file << "# e.g. \"V CS+Q\" == Ctrl + Shift + 'Q'\n";
    file << "\n";
    file << "# ONLY Accelerator Keys with corresponding alphanumeric characters can be\n";
    file << "# updated i.e. function keys (F2), special keys (Delete, HoMe) etc. will not.\n";
    file << "\n";
    file << "# ONLY change the msgstr field. Do NOT change any other.\n";
    file << "# If you do not want to change an Accelerator Key, copy msgid to msgstr\n";
    file << "\n";

    for (auto I = this->cbegin(); I != this->cend(); ++I)
    {
        std::wstring s = I->first;
        s.erase(s.cbegin(), std::find_if(s.cbegin(), s.cend(), [](const auto& c) { return !iswspace(c); }));
        if (s.empty())
            continue;

        TagResourceEntry entry = I->second;
        for (auto II = entry.automaticComments.cbegin(); II != entry.automaticComments.cend(); ++II)
        {
            file << CUnicodeUtils::StdGetUTF8(*II) << "\n";
        }
        for (auto II = entry.translatorComments.cbegin(); II != entry.translatorComments.cend(); ++II)
        {
            file << CUnicodeUtils::StdGetUTF8(*II) << "\n";
        }
        if (!I->second.resourceIDs.empty())
        {
            file << "#. Resource IDs: (";

            auto II = I->second.resourceIDs.begin();
            file << CUnicodeUtils::StdGetUTF8(*II);
            ++II;
            while (II != I->second.resourceIDs.end())
            {
                file << ", ";
                file << CUnicodeUtils::StdGetUTF8(*II);
                ++II;
            };
            file << ")\n";
        }
        if (I->second.flag.length() > 0)
            file << (CUnicodeUtils::StdGetUTF8(I->second.flag)) << "\n";
        file << ("msgid \"") << CUnicodeUtils::StdGetUTF8(I->first) << "\"\n";
        file << ("msgstr \"") << CUnicodeUtils::StdGetUTF8(I->second.msgStr) << "\"\n\n";
        nEntries++;
    }
    file.close();
    if (!m_bQuiet)
        fwprintf(stdout, L"File %s saved, containing %d entries\n", szPath, nEntries);
    return TRUE;
}

void CPOFile::AdjustEOLs(std::wstring& str)
{
    std::wstring            result;
    std::wstring::size_type pos = 0;
    for (;;) // while (true)
    {
        std::wstring::size_type next = str.find(L"\\r\\n", pos);
        result.append(str, pos, next - pos);
        if (next != std::string::npos)
        {
            result.append(L"\\n");
            pos = next + 4; // 4 = sizeof("\\r\\n")
        }
        else
        {
            break; // exit loop
        }
    }
    str.swap(result);
    result.clear();
    pos = 0;

    for (;;) // while (true)
    {
        std::wstring::size_type next = str.find(L"\\n", pos);
        result.append(str, pos, next - pos);
        if (next != std::string::npos)
        {
            result.append(L"\\r\\n");
            pos = next + 2; // 2 = sizeof("\\n")
        }
        else
        {
            break; // exit loop
        }
    }
    str.swap(result);
}
