/*
 * PROJECT:    ReactOS Notepad
 * LICENSE:    LGPL-2.1-or-later (https://spdx.org/licenses/LGPL-2.1-or-later)
 * PURPOSE:    Providing a Windows-compatible simple text editor for ReactOS
 * COPYRIGHT:  Copyright 1998,99 Marcel Baur <mbaur@g26.ethz.ch>
 *             Copyright 2002 Sylvain Petreolle <spetreolle@yahoo.fr>
 *             Copyright 2002 Andriy Palamarchuk
 *             Copyright 2019-2026 Katayama Hirofumi MZ <katayama.hirofumi.mz@gmail.com>
 */

#include "notepad.h"
#include <assert.h>

static BOOL IsTextNonZeroASCII(LPCVOID pText, DWORD dwSize)
{
    const signed char *pch = pText;
    while (dwSize-- > 0)
    {
        if (*pch <= 0)
            return FALSE;

        ++pch;
    }
    return TRUE;
}

static ENCODING AnalyzeEncoding(const BYTE *pBytes, DWORD dwSize)
{
    INT flags = IS_TEXT_UNICODE_STATISTICS | IS_TEXT_UNICODE_REVERSE_STATISTICS;

    if (IsTextNonZeroASCII(pBytes, dwSize))
        return ENCODING_DEFAULT;

    if (IsTextUnicode(pBytes, dwSize, &flags))
        return ENCODING_UTF16LE;

    if (((flags & IS_TEXT_UNICODE_REVERSE_MASK) == IS_TEXT_UNICODE_REVERSE_STATISTICS))
        return ENCODING_UTF16BE;

    /* is it UTF-8? */
    if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, (LPCSTR)pBytes, dwSize, NULL, 0))
        return ENCODING_UTF8;

    return ENCODING_ANSI;
}

static VOID
ReplaceNewLines(LPWSTR pszNew, SIZE_T cchNew, LPCWSTR pszOld, SIZE_T cchOld)
{
    BOOL bPrevCR = FALSE;
    SIZE_T ichNew, ichOld;

    for (ichOld = ichNew = 0; ichOld < cchOld; ++ichOld)
    {
        WCHAR ch = pszOld[ichOld];

        if (ch == L'\n')
        {
            if (!bPrevCR)
            {
                pszNew[ichNew++] = L'\r';
                pszNew[ichNew++] = L'\n';
            }
        }
        else if (ch == '\r')
        {
            pszNew[ichNew++] = L'\r';
            pszNew[ichNew++] = L'\n';
        }
        else
        {
            pszNew[ichNew++] = ch;
        }

        bPrevCR = (ch == L'\r');
    }

    pszNew[ichNew] = UNICODE_NULL;
    assert(ichNew == cchNew);
}

static BOOL
ProcessNewLinesAndNulls(HLOCAL *phLocal, LPWSTR *ppszText, SIZE_T *pcchText, EOLN *piEoln)
{
    SIZE_T ich, cchText = *pcchText, adwEolnCount[3] = { 0, 0, 0 }, cNonCRLFs, cchNew;
    LPWSTR pszText = *ppszText, pszNew;
    EOLN iEoln;
    BOOL bPrevCR = FALSE;
    HLOCAL hLocal;

    /* Replace '\0' with SPACE. Count newlines. */
    for (ich = 0; ich < cchText; ++ich)
    {
        WCHAR ch = pszText[ich];
        if (ch == UNICODE_NULL)
            pszText[ich] = L' ';

        if (ch == L'\n')
        {
            if (bPrevCR)
            {
                adwEolnCount[EOLN_CR]--;
                adwEolnCount[EOLN_CRLF]++;
            }
            else
            {
                adwEolnCount[EOLN_LF]++;
            }
        }
        else if (ch == '\r')
        {
            adwEolnCount[EOLN_CR]++;
        }

        bPrevCR = (ch == L'\r');
    }

    /* Choose the newline code */
    if (adwEolnCount[EOLN_CR] > adwEolnCount[EOLN_CRLF])
        iEoln = EOLN_CR;
    else if (adwEolnCount[EOLN_LF] > adwEolnCount[EOLN_CRLF])
        iEoln = EOLN_LF;
    else
        iEoln = EOLN_CRLF;

    cNonCRLFs = adwEolnCount[EOLN_CR] + adwEolnCount[EOLN_LF];
    if (cNonCRLFs != 0)
    {
        /* Allocate a buffer for EM_SETHANDLE */
        cchNew = cchText + cNonCRLFs;
        hLocal = LocalAlloc(LMEM_MOVEABLE, (cchNew + 1) * sizeof(WCHAR));
        if (!hLocal)
        {
            SetLastError(ERROR_NOT_ENOUGH_MEMORY);
            return FALSE; /* Failure */
        }

        pszNew = LocalLock(hLocal);
        if (!pszNew)
        {
            LocalFree(hLocal);
            SetLastError(ERROR_NOT_ENOUGH_MEMORY);
            return FALSE; /* Failure */
        }

        ReplaceNewLines(pszNew, cchNew, pszText, cchText);

        /* Replace with new data */
        LocalUnlock(*phLocal);
        LocalFree(*phLocal);
        *phLocal = hLocal;
        *ppszText = pszNew;
        *pcchText = cchNew;
    }

    *piEoln = iEoln;
    return TRUE;
}

HLOCAL
ReadText(HANDLE hFile, ENCODING *pencFile, EOLN *piEoln)
{
    LPBYTE pBytes = NULL;
    LPWSTR pszText, pszNewText = NULL;
    DWORD dwSize, dwPos;
    SIZE_T i, cchText, cbContent;
    BOOL bSuccess = FALSE;
    ENCODING encFile;
    UINT iCodePage;
    HANDLE hMapping = INVALID_HANDLE_VALUE;
    HLOCAL hNewLocal = NULL;

    dwSize = GetFileSize(hFile, NULL);
    if (dwSize == INVALID_FILE_SIZE)
        goto done;

    if (dwSize == 0) // If file is empty
    {
        hNewLocal = LocalAlloc(LMEM_MOVEABLE, sizeof(UNICODE_NULL));
        if (!hNewLocal)
        {
            SetLastError(ERROR_NOT_ENOUGH_MEMORY);
            goto done;
        }

        pszNewText = LocalLock(hNewLocal);
        if (!pszNewText)
        {
            SetLastError(ERROR_NOT_ENOUGH_MEMORY);
            goto done;
        }

        *pszNewText = UNICODE_NULL;
        LocalUnlock(hNewLocal);

        *piEoln = EOLN_CRLF;
        *pencFile = ENCODING_DEFAULT;
        return hNewLocal;
    }

    hMapping = CreateFileMappingW(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
    if (hMapping == NULL)
        goto done;

    pBytes = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, dwSize);
    if (!pBytes)
        goto done;

    /* Look for Byte Order Marks */
    dwPos = 0;
    if ((dwSize >= 2) && (pBytes[0] == 0xFF) && (pBytes[1] == 0xFE))
    {
        encFile = ENCODING_UTF16LE;
        dwPos += 2;
    }
    else if ((dwSize >= 2) && (pBytes[0] == 0xFE) && (pBytes[1] == 0xFF))
    {
        encFile = ENCODING_UTF16BE;
        dwPos += 2;
    }
    else if ((dwSize >= 3) && (pBytes[0] == 0xEF) && (pBytes[1] == 0xBB) && (pBytes[2] == 0xBF))
    {
        encFile = ENCODING_UTF8BOM;
        dwPos += 3;
    }
    else
    {
        encFile = AnalyzeEncoding(pBytes, dwSize);
    }

    switch(encFile)
    {
        case ENCODING_UTF16BE:
        case ENCODING_UTF16LE:
        {
            /* Allocate the buffer for EM_SETHANDLE */
            pszText = (LPWSTR)&pBytes[dwPos];
            cchText = (dwSize - dwPos) / sizeof(WCHAR);
            if (cchText >= MAXLONG / sizeof(WCHAR))
            {
                SetLastError(ERROR_FILE_TOO_LARGE);
                goto done;
            }

            hNewLocal = LocalAlloc(LMEM_MOVEABLE, (cchText + 1) * sizeof(WCHAR));
            if (!hNewLocal)
            {
                SetLastError(ERROR_NOT_ENOUGH_MEMORY);
                goto done;
            }

            pszNewText = LocalLock(hNewLocal);
            if (!pszNewText)
            {
                SetLastError(ERROR_NOT_ENOUGH_MEMORY);
                goto done;
            }

            CopyMemory(pszNewText, pszText, cchText * sizeof(WCHAR));

            if (encFile == ENCODING_UTF16BE) /* big endian; Swap bytes */
            {
                BYTE tmp, *pb = (LPBYTE)pszNewText;
                for (i = 0; i < cchText * 2; i += 2)
                {
                    tmp = pb[i];
                    pb[i] = pb[i + 1];
                    pb[i + 1] = tmp;
                }
            }
            break;
        }

        case ENCODING_ANSI:
        case ENCODING_UTF8:
        case ENCODING_UTF8BOM:
        {
            iCodePage = ((encFile == ENCODING_UTF8 || encFile == ENCODING_UTF8BOM)
                         ? CP_UTF8 : CP_ACP);
            cbContent = dwSize - dwPos;
            if (cbContent >= MAXLONG / sizeof(WCHAR))
            {
                SetLastError(ERROR_FILE_TOO_LARGE);
                goto done;
            }

            /* Allocate the buffer for EM_SETHANDLE */
            hNewLocal = LocalAlloc(LMEM_MOVEABLE, (cbContent + 1) * sizeof(WCHAR));
            if (!hNewLocal)
            {
                SetLastError(ERROR_NOT_ENOUGH_MEMORY);
                goto done;
            }

            pszNewText = LocalLock(hNewLocal);
            if (!pszNewText)
            {
                SetLastError(ERROR_NOT_ENOUGH_MEMORY);
                goto done;
            }

            /* Do conversion */
            cchText = 0;
            if (cbContent > 0)
            {
                cchText = MultiByteToWideChar(iCodePage, 0,
                                              (LPCSTR)&pBytes[dwPos], (INT)cbContent,
                                              pszNewText, (INT)cbContent);
                if (!cchText)
                    goto done;
            }
            break;
        }

        DEFAULT_UNREACHABLE;
    }

    pszNewText[cchText] = UNICODE_NULL;

    if (!ProcessNewLinesAndNulls(&hNewLocal, &pszNewText, &cchText, piEoln))
        goto done;

    *pencFile = encFile;
    bSuccess = TRUE;

done:
    if (pBytes)
        UnmapViewOfFile(pBytes);
    if (hMapping != INVALID_HANDLE_VALUE)
        CloseHandle(hMapping);
    if (pszNewText)
        LocalUnlock(hNewLocal);
    if (!bSuccess && hNewLocal)
    {
        LocalFree(hNewLocal);
        hNewLocal = NULL;
    }
    return hNewLocal;
}

static BOOL WriteEncodedText(HANDLE hFile, LPCWSTR pszText, DWORD dwTextLen, ENCODING encFile)
{
    LPBYTE pBytes = NULL;
    LPBYTE pAllocBuffer = NULL;
    DWORD dwPos = 0;
    DWORD dwByteCount;
    BYTE buffer[1024];
    UINT iCodePage = 0;
    DWORD dwDummy, i;
    BOOL bSuccess = FALSE;
    int iBufferSize, iRequiredBytes;
    BYTE b;

    while(dwPos < dwTextLen)
    {
        switch(encFile)
        {
            case ENCODING_UTF16LE:
                pBytes = (LPBYTE) &pszText[dwPos];
                dwByteCount = (dwTextLen - dwPos) * sizeof(WCHAR);
                dwPos = dwTextLen;
                break;

            case ENCODING_UTF16BE:
                dwByteCount = (dwTextLen - dwPos) * sizeof(WCHAR);
                if (dwByteCount > sizeof(buffer))
                    dwByteCount = sizeof(buffer);

                memcpy(buffer, &pszText[dwPos], dwByteCount);
                for (i = 0; i < dwByteCount; i += 2)
                {
                    b = buffer[i+0];
                    buffer[i+0] = buffer[i+1];
                    buffer[i+1] = b;
                }
                pBytes = (LPBYTE) &buffer[dwPos];
                dwPos += dwByteCount / sizeof(WCHAR);
                break;

            case ENCODING_ANSI:
            case ENCODING_UTF8:
            case ENCODING_UTF8BOM:
                if (encFile == ENCODING_UTF8 || encFile == ENCODING_UTF8BOM)
                    iCodePage = CP_UTF8;
                else
                    iCodePage = CP_ACP;

                iRequiredBytes = WideCharToMultiByte(iCodePage, 0, &pszText[dwPos], dwTextLen - dwPos, NULL, 0, NULL, NULL);
                if (iRequiredBytes <= 0)
                {
                    goto done;
                }
                else if (iRequiredBytes < sizeof(buffer))
                {
                    pBytes = buffer;
                    iBufferSize = sizeof(buffer);
                }
                else
                {
                    pAllocBuffer = (LPBYTE) HeapAlloc(GetProcessHeap(), 0, iRequiredBytes);
                    if (!pAllocBuffer)
                        return FALSE;
                    pBytes = pAllocBuffer;
                    iBufferSize = iRequiredBytes;
                }

                dwByteCount = WideCharToMultiByte(iCodePage, 0, &pszText[dwPos], dwTextLen - dwPos, (LPSTR) pBytes, iBufferSize, NULL, NULL);
                if (!dwByteCount)
                    goto done;

                dwPos = dwTextLen;
                break;

            default:
                goto done;
        }

        if (!WriteFile(hFile, pBytes, dwByteCount, &dwDummy, NULL))
            goto done;

        /* free the buffer, if we have allocated one */
        if (pAllocBuffer)
        {
            HeapFree(GetProcessHeap(), 0, pAllocBuffer);
            pAllocBuffer = NULL;
        }
    }
    bSuccess = TRUE;

done:
    if (pAllocBuffer)
        HeapFree(GetProcessHeap(), 0, pAllocBuffer);
    return bSuccess;
}

BOOL WriteText(HANDLE hFile, LPCWSTR pszText, DWORD dwTextLen, ENCODING encFile, EOLN iEoln)
{
    WCHAR wcBom;
    LPCWSTR pszLF = L"\n";
    DWORD dwPos, dwNext;

    /* Write the proper byte order marks if not ANSI or UTF-8 without BOM */
    if (encFile != ENCODING_ANSI && encFile != ENCODING_UTF8)
    {
        wcBom = 0xFEFF;
        if (!WriteEncodedText(hFile, &wcBom, 1, encFile))
            return FALSE;
    }

    dwPos = 0;

    /* pszText eoln are always \r\n */

    do
    {
        /* Find the next eoln */
        dwNext = dwPos;
        while(dwNext < dwTextLen)
        {
            if (pszText[dwNext] == '\r' && pszText[dwNext + 1] == '\n')
                break;
            dwNext++;
        }

        if (dwNext != dwTextLen)
        {
            switch (iEoln)
            {
            case EOLN_LF:
                /* Write text (without eoln) */
                if (!WriteEncodedText(hFile, &pszText[dwPos], dwNext - dwPos, encFile))
                    return FALSE;
                /* Write eoln */
                if (!WriteEncodedText(hFile, pszLF, 1, encFile))
                    return FALSE;
                break;
            case EOLN_CR:
                /* Write text (including \r as eoln) */
                if (!WriteEncodedText(hFile, &pszText[dwPos], dwNext - dwPos + 1, encFile))
                    return FALSE;
                break;
            case EOLN_CRLF:
                /* Write text (including \r\n as eoln) */
                if (!WriteEncodedText(hFile, &pszText[dwPos], dwNext - dwPos + 2, encFile))
                    return FALSE;
                break;
            default:
                return FALSE;
            }
        }
        else
        {
            /* Write text (without eoln, since this is the end of the file) */
            if (!WriteEncodedText(hFile, &pszText[dwPos], dwNext - dwPos, encFile))
                return FALSE;
        }

        /* Skip \r\n */
        dwPos = dwNext + 2;
    }
    while (dwPos < dwTextLen);

    return TRUE;
}
