#define UNICODE
#define _UNICODE
#define __STDC_WANT_LIB_EXT1__ 1
#include <windows.h>
#include <commdlg.h>
#include <stdbool.h>
#include <stdio.h>
#include <wchar.h>
#include <wctype.h>
#include "resource.h"

#ifndef swprintf_s
int swprintf_s(wchar_t *buffer, size_t sizeOfBuffer, const wchar_t *format, ...);
#endif

#define IDM_FILE_NEW     1001
#define IDM_FILE_OPEN    1002
#define IDM_FILE_SAVE    1003
#define IDM_FILE_SAVE_AS 1004
#define IDM_FILE_EXIT    1005

#define IDM_EDIT_UNDO    2001
#define IDM_EDIT_CUT     2002
#define IDM_EDIT_COPY    2003
#define IDM_EDIT_PASTE   2004
#define IDM_EDIT_DELETE  2005
#define IDM_EDIT_SELECT  2006
#define IDM_EDIT_TIME    2007
#define IDM_EDIT_FIND    2008
#define IDM_EDIT_FIND_NEXT 2009
#define IDM_EDIT_REPLACE 2010

#define IDM_FORMAT_WRAP  3001
#define IDM_FORMAT_FONT  3002

#define IDM_HELP_ABOUT   4001

static const WCHAR APP_VERSION[] = L"1.3.0";

static HWND g_hEdit = NULL;
static HFONT g_hFont = NULL;
static HMENU g_hMenu = NULL;
static HACCEL g_hAccel = NULL;
static bool g_wordWrap = false;
static WCHAR g_filePath[MAX_PATH] = L"";
static LOGFONTW g_logFont = {0};
static UINT g_msgFindReplace = 0;
static FINDREPLACEW g_fr = {0};
static WCHAR g_findText[128] = L"";
static WCHAR g_replaceText[128] = L"";
static DWORD g_findFlags = FR_DOWN;
static HWND g_hFindDlg = NULL;
static WNDPROC g_findDlgOldProc = NULL;

#define IDC_FIND_ALL 6001
#ifndef edt1
#define edt1 0x0480
#endif
#ifndef chx1
#define chx1 0x0430
#endif
#ifndef chx2
#define chx2 0x0431
#endif

static bool MatchSubstring(const WCHAR *text, const WCHAR *pattern, int patternLen, int pos, bool matchCase);
static void EnsureFindAllButton(void);

#ifndef ARRAYSIZE
#define ARRAYSIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
#endif

int swprintf_s(wchar_t *buffer, size_t sizeOfBuffer, const wchar_t *format, ...);

static void CopyStringSafe(WCHAR *dest, size_t destCount, const WCHAR *src)
{
    if (!dest || destCount == 0)
    {
        return;
    }
    if (!src)
    {
        dest[0] = L'\0';
        return;
    }

    wcsncpy(dest, src, destCount - 1);
    dest[destCount - 1] = L'\0';
}

static void UpdateTitle(HWND hwnd)
{
    const WCHAR *name = g_filePath;
    const WCHAR *lastSlash = wcsrchr(g_filePath, L'\\');
    if (lastSlash && *(lastSlash + 1) != L'\0')
    {
        name = lastSlash + 1;
    }
    if (name[0] == L'\0')
    {
        name = L"Untitled";
    }

    WCHAR title[512];
    swprintf_s(title, ARRAYSIZE(title), L"%s - ClassicNotepad", name);
    SetWindowTextW(hwnd, title);
}

static void SetDefaultFont(HWND hwnd)
{
    if (!g_hFont)
    {
        if (!SystemParametersInfoW(SPI_GETICONTITLELOGFONT, sizeof(g_logFont), &g_logFont, 0))
        {
            ZeroMemory(&g_logFont, sizeof(g_logFont));
            g_logFont.lfHeight = -11;
            wcscpy(g_logFont.lfFaceName, L"Segoe UI");
        }
        g_hFont = CreateFontIndirectW(&g_logFont);
    }
    SendMessage(hwnd, WM_SETFONT, (WPARAM)g_hFont, TRUE);
}

static void ResizeEdit(HWND hwndParent)
{
    RECT rc;
    GetClientRect(hwndParent, &rc);
    MoveWindow(g_hEdit, 0, 0, rc.right - rc.left, rc.bottom - rc.top, TRUE);
}

static void RecreateEdit(HWND hwndParent)
{
    int len = GetWindowTextLengthW(g_hEdit);
    WCHAR *buffer = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, (len + 1) * sizeof(WCHAR));
    if (buffer)
    {
        GetWindowTextW(g_hEdit, buffer, len + 1);
    }

    DestroyWindow(g_hEdit);

    DWORD styles = WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP | ES_LEFT | ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN;
    if (!g_wordWrap)
    {
        styles |= WS_HSCROLL | ES_AUTOHSCROLL;
    }

    g_hEdit = CreateWindowExW(0, L"EDIT", NULL, styles, 0, 0, 0, 0, hwndParent, (HMENU)1, GetModuleHandle(NULL), NULL);
    SetDefaultFont(g_hEdit);
    if (buffer)
    {
        SetWindowTextW(g_hEdit, buffer);
        SendMessageW(g_hEdit, EM_SETSEL, (WPARAM)len, (LPARAM)len);
        HeapFree(GetProcessHeap(), 0, buffer);
    }
    ResizeEdit(hwndParent);
}

static HMENU BuildMenu(void)
{
    HMENU hMenu = CreateMenu();

    HMENU hFile = CreatePopupMenu();
    AppendMenuW(hFile, MF_STRING, IDM_FILE_NEW, L"&New\tCtrl+N");
    AppendMenuW(hFile, MF_STRING, IDM_FILE_OPEN, L"&Open...\tCtrl+O");
    AppendMenuW(hFile, MF_STRING, IDM_FILE_SAVE, L"&Save\tCtrl+S");
    AppendMenuW(hFile, MF_STRING, IDM_FILE_SAVE_AS, L"Save &As...");
    AppendMenuW(hFile, MF_SEPARATOR, 0, NULL);
    AppendMenuW(hFile, MF_STRING, IDM_FILE_EXIT, L"E&xit");

    HMENU hEdit = CreatePopupMenu();
    AppendMenuW(hEdit, MF_STRING, IDM_EDIT_UNDO, L"&Undo\tCtrl+Z");
    AppendMenuW(hEdit, MF_SEPARATOR, 0, NULL);
    AppendMenuW(hEdit, MF_STRING, IDM_EDIT_CUT, L"Cu&t\tCtrl+X");
    AppendMenuW(hEdit, MF_STRING, IDM_EDIT_COPY, L"&Copy\tCtrl+C");
    AppendMenuW(hEdit, MF_STRING, IDM_EDIT_PASTE, L"&Paste\tCtrl+V");
    AppendMenuW(hEdit, MF_STRING, IDM_EDIT_DELETE, L"&Delete\tDel");
    AppendMenuW(hEdit, MF_SEPARATOR, 0, NULL);
    AppendMenuW(hEdit, MF_STRING, IDM_EDIT_FIND, L"&Find...\tCtrl+F");
    AppendMenuW(hEdit, MF_STRING, IDM_EDIT_FIND_NEXT, L"Find &Next\tF3");
    AppendMenuW(hEdit, MF_STRING, IDM_EDIT_REPLACE, L"&Replace...\tCtrl+H");
    AppendMenuW(hEdit, MF_SEPARATOR, 0, NULL);
    AppendMenuW(hEdit, MF_STRING, IDM_EDIT_SELECT, L"Select &All\tCtrl+A");
    AppendMenuW(hEdit, MF_STRING, IDM_EDIT_TIME, L"Time/&Date\tF5");

    HMENU hFormat = CreatePopupMenu();
    AppendMenuW(hFormat, MF_STRING | (g_wordWrap ? MF_CHECKED : 0), IDM_FORMAT_WRAP, L"&Word Wrap");
    AppendMenuW(hFormat, MF_STRING, IDM_FORMAT_FONT, L"&Font...");

    HMENU hHelp = CreatePopupMenu();
    AppendMenuW(hHelp, MF_STRING, IDM_HELP_ABOUT, L"&About ClassicNotepad");

    AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hFile, L"&File");
    AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hEdit, L"&Edit");
    AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hFormat, L"F&ormat");
    AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hHelp, L"&Help");

    return hMenu;
}

static void UpdateWrapMenu(void)
{
    HMENU hFormat = GetSubMenu(g_hMenu, 2);
    CheckMenuItem(hFormat, IDM_FORMAT_WRAP, MF_BYCOMMAND | (g_wordWrap ? MF_CHECKED : MF_UNCHECKED));
}

static bool ShowOpenDialog(HWND hwnd, WCHAR *pathBuffer, DWORD cchBuffer)
{
    pathBuffer[0] = L'\0';
    OPENFILENAMEW ofn = {0};
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = hwnd;
    ofn.lpstrFilter = L"Text Files (*.txt)\0*.txt\0All Files (*.*)\0*.*\0";
    ofn.lpstrFile = pathBuffer;
    ofn.nMaxFile = cchBuffer;
    ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER;
    ofn.lpstrDefExt = L"txt";
    return GetOpenFileNameW(&ofn);
}

static bool ShowSaveDialog(HWND hwnd)
{
    OPENFILENAMEW ofn = {0};
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = hwnd;
    ofn.lpstrFilter = L"Text Files (*.txt)\0*.txt\0All Files (*.*)\0*.*\0";
    ofn.lpstrFile = g_filePath;
    ofn.nMaxFile = MAX_PATH;
    ofn.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_EXPLORER;
    ofn.lpstrDefExt = L"txt";
    return GetSaveFileNameW(&ofn);
}

static bool LoadFileToEdit(HWND hwnd, const WCHAR *path)
{
    HANDLE hFile = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE)
    {
        MessageBoxW(hwnd, L"Unable to open the file.", L"Error", MB_ICONERROR | MB_OK);
        return false;
    }

    LARGE_INTEGER size;
    if (!GetFileSizeEx(hFile, &size) || size.QuadPart > 20 * 1024 * 1024)
    {
        CloseHandle(hFile);
        MessageBoxW(hwnd, L"File is too large to open.", L"Error", MB_ICONERROR | MB_OK);
        return false;
    }

    DWORD bytesToRead = (DWORD)size.QuadPart;
    BYTE *buffer = (BYTE *)HeapAlloc(GetProcessHeap(), 0, bytesToRead + 3);
    DWORD bytesRead = 0;
    bool success = false;

    if (buffer && ReadFile(hFile, buffer, bytesToRead, &bytesRead, NULL))
    {
        int skip = 0;
        UINT codePage = CP_UTF8;
        if (bytesRead >= 3 && buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF)
        {
            skip = 3;
        }
        else if (bytesRead >= 2 && buffer[0] == 0xFF && buffer[1] == 0xFE)
        {
            codePage = 1200; // UTF-16 LE
        }
        else if (bytesRead >= 2 && buffer[0] == 0xFE && buffer[1] == 0xFF)
        {
            codePage = 1201; // UTF-16 BE
        }
        else
        {
            codePage = CP_ACP;
        }

        int wideLen = MultiByteToWideChar(codePage, 0, (LPCCH)(buffer + skip), bytesRead - skip, NULL, 0);
        WCHAR *wbuf = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, (wideLen + 1) * sizeof(WCHAR));
        if (wbuf)
        {
            MultiByteToWideChar(codePage, 0, (LPCCH)(buffer + skip), bytesRead - skip, wbuf, wideLen);
            wbuf[wideLen] = L'\0';
            SetWindowTextW(g_hEdit, wbuf);
            HeapFree(GetProcessHeap(), 0, wbuf);
            success = true;
        }
    }

    if (buffer)
    {
        HeapFree(GetProcessHeap(), 0, buffer);
    }
    CloseHandle(hFile);

    if (!success)
    {
        MessageBoxW(hwnd, L"Unable to read the file.", L"Error", MB_ICONERROR | MB_OK);
    }
    return success;
}

static bool SaveFileFromEdit(HWND hwnd, const WCHAR *path)
{
    int len = GetWindowTextLengthW(g_hEdit);
    WCHAR *wbuf = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, (len + 1) * sizeof(WCHAR));
    if (!wbuf)
    {
        MessageBoxW(hwnd, L"Not enough memory to save.", L"Error", MB_ICONERROR | MB_OK);
        return false;
    }

    GetWindowTextW(g_hEdit, wbuf, len + 1);

    HANDLE hFile = CreateFileW(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE)
    {
        HeapFree(GetProcessHeap(), 0, wbuf);
        MessageBoxW(hwnd, L"Unable to create the file.", L"Error", MB_ICONERROR | MB_OK);
        return false;
    }

    // Save as UTF-8 with BOM for reliable re-open.
    BYTE bom[] = {0xEF, 0xBB, 0xBF};
    DWORD written = 0;
    bool success = false;
    if (!WriteFile(hFile, bom, sizeof(bom), &written, NULL))
    {
        MessageBoxW(hwnd, L"Failed to save the file.", L"Error", MB_ICONERROR | MB_OK);
        HeapFree(GetProcessHeap(), 0, wbuf);
        CloseHandle(hFile);
        return false;
    }

    int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wbuf, len, NULL, 0, NULL, NULL);
    if (utf8Len > 0)
    {
        BYTE *mbuf = (BYTE *)HeapAlloc(GetProcessHeap(), 0, utf8Len);
        if (mbuf)
        {
            WideCharToMultiByte(CP_UTF8, 0, wbuf, len, (LPSTR)mbuf, utf8Len, NULL, NULL);
            if (WriteFile(hFile, mbuf, utf8Len, &written, NULL))
            {
                success = true;
            }
            HeapFree(GetProcessHeap(), 0, mbuf);
        }
    }
    else
    {
        success = true; // Empty file is fine.
    }

    HeapFree(GetProcessHeap(), 0, wbuf);
    CloseHandle(hFile);

    if (!success)
    {
        MessageBoxW(hwnd, L"Failed to save the file.", L"Error", MB_ICONERROR | MB_OK);
    }
    return success;
}

static void DoFileNew(HWND hwnd)
{
    g_filePath[0] = L'\0';
    SetWindowTextW(g_hEdit, L"");
    UpdateTitle(hwnd);
}

static void DoFileOpen(HWND hwnd)
{
    WCHAR original[MAX_PATH];
    CopyStringSafe(original, ARRAYSIZE(original), g_filePath);
    if (!ShowOpenDialog(hwnd, g_filePath, MAX_PATH))
    {
        CopyStringSafe(g_filePath, ARRAYSIZE(g_filePath), original);
        return;
    }

    if (LoadFileToEdit(hwnd, g_filePath))
    {
        UpdateTitle(hwnd);
    }
    else
    {
        CopyStringSafe(g_filePath, ARRAYSIZE(g_filePath), original);
    }
}

static void DoFileSave(HWND hwnd)
{
    if (g_filePath[0] == L'\0')
    {
        if (!ShowSaveDialog(hwnd))
        {
            return;
        }
    }

    if (SaveFileFromEdit(hwnd, g_filePath))
    {
        UpdateTitle(hwnd);
    }
}

static void DoFileSaveAs(HWND hwnd)
{
    WCHAR backup[MAX_PATH];
    CopyStringSafe(backup, ARRAYSIZE(backup), g_filePath);
    if (ShowSaveDialog(hwnd))
    {
        if (SaveFileFromEdit(hwnd, g_filePath))
        {
            UpdateTitle(hwnd);
        }
        else
        {
            CopyStringSafe(g_filePath, ARRAYSIZE(g_filePath), backup);
        }
    }
}

static void InsertTimeDate(void)
{
    SYSTEMTIME st;
    GetLocalTime(&st);
    WCHAR text[64];
    swprintf_s(text, ARRAYSIZE(text), L"%02d/%02d/%04d %02d:%02d", st.wMonth, st.wDay, st.wYear, st.wHour, st.wMinute);
    SendMessageW(g_hEdit, EM_REPLACESEL, TRUE, (LPARAM)text);
}
static bool MatchSubstring(const WCHAR *text, const WCHAR *pattern, int patternLen, int pos, bool matchCase)
{
    if (pos < 0)
    {
        return false;
    }
    for (int i = 0; i < patternLen; ++i)
    {
        WCHAR a = text[pos + i];
        WCHAR b = pattern[i];
        if (!matchCase)
        {
            a = (WCHAR)towlower(a);
            b = (WCHAR)towlower(b);
        }
        if (a != b)
        {
            return false;
        }
    }
    return true;
}

static int CountOccurrences(bool matchCase)
{
    if (g_findText[0] == L'\0')
    {
        return 0;
    }

    int findLen = (int)wcslen(g_findText);
    int textLen = GetWindowTextLengthW(g_hEdit);
    if (findLen == 0 || textLen < findLen)
    {
        return 0;
    }

    WCHAR *buffer = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, (textLen + 1) * sizeof(WCHAR));
    if (!buffer)
    {
        return 0;
    }
    GetWindowTextW(g_hEdit, buffer, textLen + 1);

    int occurrences = 0;
    for (int i = 0; i <= textLen - findLen; )
    {
        if (MatchSubstring(buffer, g_findText, findLen, i, matchCase))
        {
            occurrences++;
            i += findLen;
        }
        else
        {
            ++i;
        }
    }

    HeapFree(GetProcessHeap(), 0, buffer);
    return occurrences;
}

static bool FindAndSelectText(HWND hwnd, DWORD flags, bool allowWrap)
{
    if (g_findText[0] == L'\0')
    {
        return false;
    }

    int patternLen = (int)wcslen(g_findText);
    int textLen = GetWindowTextLengthW(g_hEdit);
    if (patternLen == 0 || textLen == 0 || textLen < patternLen)
    {
        MessageBoxW(hwnd, L"Cannot find the specified text.", L"ClassicNotepad", MB_OK | MB_ICONINFORMATION);
        return false;
    }

    WCHAR *buffer = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, (textLen + 1) * sizeof(WCHAR));
    if (!buffer)
    {
        return false;
    }
    GetWindowTextW(g_hEdit, buffer, textLen + 1);

    DWORD selStart = 0;
    DWORD selEnd = 0;
    SendMessageW(g_hEdit, EM_GETSEL, (WPARAM)&selStart, (LPARAM)&selEnd);

    bool searchDown = (flags & FR_DOWN) != 0;
    bool matchCase = (flags & FR_MATCHCASE) != 0;
    int found = -1;

    if (searchDown)
    {
        for (int i = (int)selEnd; i <= textLen - patternLen; ++i)
        {
            if (MatchSubstring(buffer, g_findText, patternLen, i, matchCase))
            {
                found = i;
                break;
            }
        }
        if (found < 0 && allowWrap)
        {
            for (int i = 0; i < (int)selEnd && i <= textLen - patternLen; ++i)
            {
                if (MatchSubstring(buffer, g_findText, patternLen, i, matchCase))
                {
                    found = i;
                    break;
                }
            }
        }
    }
    else
    {
        int startPos = (int)selStart - patternLen;
        if (startPos > textLen - patternLen)
        {
            startPos = textLen - patternLen;
        }
        for (int i = startPos; i >= 0; --i)
        {
            if (MatchSubstring(buffer, g_findText, patternLen, i, matchCase))
            {
                found = i;
                break;
            }
        }
        if (found < 0 && allowWrap)
        {
            for (int i = textLen - patternLen; i > startPos; --i)
            {
                if (MatchSubstring(buffer, g_findText, patternLen, i, matchCase))
                {
                    found = i;
                    break;
                }
            }
        }
    }

    HeapFree(GetProcessHeap(), 0, buffer);

    if (found >= 0)
    {
        SendMessageW(g_hEdit, EM_SETSEL, (WPARAM)found, (LPARAM)(found + patternLen));
        SendMessageW(g_hEdit, EM_SCROLLCARET, 0, 0);
        return true;
    }

    MessageBoxW(hwnd, L"Cannot find the specified text.", L"ClassicNotepad", MB_OK | MB_ICONINFORMATION);
    return false;
}

static void ShowFindOrReplaceDialog(HWND hwnd, bool replace)
{
    if (g_hFindDlg)
    {
        SetFocus(g_hFindDlg);
        return;
    }

    ZeroMemory(&g_fr, sizeof(g_fr));
    g_fr.lStructSize = sizeof(g_fr);
    g_fr.hwndOwner = hwnd;
    g_fr.lpstrFindWhat = g_findText;
    g_fr.wFindWhatLen = ARRAYSIZE(g_findText);
    g_fr.lpstrReplaceWith = g_replaceText;
    g_fr.wReplaceWithLen = ARRAYSIZE(g_replaceText);
    g_fr.Flags = g_findFlags;

    g_hFindDlg = replace ? ReplaceTextW(&g_fr) : FindTextW(&g_fr);
    EnsureFindAllButton();
}

static void HandleReplaceCurrent(HWND hwnd)
{
    if (g_findText[0] == L'\0')
    {
        return;
    }

    DWORD selStart = 0;
    DWORD selEnd = 0;
    SendMessageW(g_hEdit, EM_GETSEL, (WPARAM)&selStart, (LPARAM)&selEnd);
    int selLen = (int)(selEnd - selStart);
    int patternLen = (int)wcslen(g_findText);

    if (selLen == patternLen && selLen > 0)
    {
        int textLen = GetWindowTextLengthW(g_hEdit);
        WCHAR *buffer = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, (textLen + 1) * sizeof(WCHAR));
        if (buffer)
        {
            GetWindowTextW(g_hEdit, buffer, textLen + 1);
            if (MatchSubstring(buffer, g_findText, patternLen, (int)selStart, (g_findFlags & FR_MATCHCASE) != 0))
            {
                SendMessageW(g_hEdit, EM_REPLACESEL, TRUE, (LPARAM)g_replaceText);
                DWORD newEnd = selStart + (DWORD)wcslen(g_replaceText);
                SendMessageW(g_hEdit, EM_SETSEL, selStart, newEnd);
                SendMessageW(g_hEdit, EM_SCROLLCARET, 0, 0);
                FindAndSelectText(hwnd, g_findFlags, true);
                HeapFree(GetProcessHeap(), 0, buffer);
                return;
            }
            HeapFree(GetProcessHeap(), 0, buffer);
        }
    }

    if (FindAndSelectText(hwnd, g_findFlags, true))
    {
        SendMessageW(g_hEdit, EM_REPLACESEL, TRUE, (LPARAM)g_replaceText);
        DWORD newEnd = 0;
        DWORD newStart = 0;
        SendMessageW(g_hEdit, EM_GETSEL, (WPARAM)&newStart, (LPARAM)&newEnd);
        SendMessageW(g_hEdit, EM_SCROLLCARET, 0, 0);
        FindAndSelectText(hwnd, g_findFlags, true);
    }
}

static void ReplaceAllOccurrences(HWND hwnd)
{
    if (g_findText[0] == L'\0')
    {
        return;
    }

    int findLen = (int)wcslen(g_findText);
    int replaceLen = (int)wcslen(g_replaceText);
    if (findLen == 0)
    {
        return;
    }

    int textLen = GetWindowTextLengthW(g_hEdit);
    WCHAR *buffer = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, (textLen + 1) * sizeof(WCHAR));
    if (!buffer)
    {
        return;
    }
    GetWindowTextW(g_hEdit, buffer, textLen + 1);

    bool matchCase = (g_findFlags & FR_MATCHCASE) != 0;
    int occurrences = 0;
    for (int i = 0; i <= textLen - findLen; )
    {
        if (MatchSubstring(buffer, g_findText, findLen, i, matchCase))
        {
            occurrences++;
            i += findLen;
        }
        else
        {
            ++i;
        }
    }

    if (occurrences == 0)
    {
        HeapFree(GetProcessHeap(), 0, buffer);
        MessageBoxW(hwnd, L"Cannot find the specified text.", L"ClassicNotepad", MB_OK | MB_ICONINFORMATION);
        return;
    }

    int newLen = textLen + occurrences * (replaceLen - findLen);
    if (newLen < 0)
    {
        HeapFree(GetProcessHeap(), 0, buffer);
        return;
    }

    WCHAR *outBuffer = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, ((size_t)newLen + 1) * sizeof(WCHAR));
    if (!outBuffer)
    {
        HeapFree(GetProcessHeap(), 0, buffer);
        return;
    }

    int outPos = 0;
    for (int i = 0; i < textLen; )
    {
        if (i <= textLen - findLen && MatchSubstring(buffer, g_findText, findLen, i, matchCase))
        {
            memcpy(outBuffer + outPos, g_replaceText, replaceLen * sizeof(WCHAR));
            outPos += replaceLen;
            i += findLen;
        }
        else
        {
            outBuffer[outPos++] = buffer[i++];
        }
    }
    outBuffer[outPos] = L'\0';

    SetWindowTextW(g_hEdit, outBuffer);
    SendMessageW(g_hEdit, EM_SETSEL, 0, 0);

    HeapFree(GetProcessHeap(), 0, outBuffer);
    HeapFree(GetProcessHeap(), 0, buffer);
}

static LRESULT CALLBACK FindDialogSubclassProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_COMMAND:
        if (LOWORD(wParam) == IDC_FIND_ALL)
        {
            GetDlgItemTextW(hDlg, edt1, g_findText, ARRAYSIZE(g_findText));
            bool matchCaseChecked = (IsDlgButtonChecked(hDlg, chx1) == BST_CHECKED) || (IsDlgButtonChecked(hDlg, chx2) == BST_CHECKED) || (IsDlgButtonChecked(hDlg, 0x0410) == BST_CHECKED);
            g_findFlags = (g_findFlags & ~FR_MATCHCASE) | (matchCaseChecked ? FR_MATCHCASE : 0);

            int count = CountOccurrences((g_findFlags & FR_MATCHCASE) != 0);
            SendMessageW(g_hEdit, EM_SETSEL, 0, 0);
            FindAndSelectText(GetParent(hDlg), g_findFlags, true);
            WCHAR buf[128];
            swprintf_s(buf, ARRAYSIZE(buf), L"Found %d occurrence%s.", count, count == 1 ? L"" : L"s");
            MessageBoxW(hDlg, buf, L"Find All", MB_OK | MB_ICONINFORMATION);
            SetFocus(g_hEdit);
            return 0;
        }
        break;
    case WM_NCDESTROY:
        if (g_findDlgOldProc)
        {
            SetWindowLongPtrW(hDlg, GWLP_WNDPROC, (LONG_PTR)g_findDlgOldProc);
            g_findDlgOldProc = NULL;
        }
        break;
    }
    return CallWindowProcW(g_findDlgOldProc ? g_findDlgOldProc : DefWindowProcW, hDlg, msg, wParam, lParam);
}

static void EnsureFindAllButton(void)
{
    if (!g_hFindDlg)
    {
        return;
    }

    if (g_findDlgOldProc == NULL)
    {
        g_findDlgOldProc = (WNDPROC)SetWindowLongPtrW(g_hFindDlg, GWLP_WNDPROC, (LONG_PTR)FindDialogSubclassProc);
    }

    if (!GetDlgItem(g_hFindDlg, IDC_FIND_ALL))
    {
        HWND hFindNext = GetDlgItem(g_hFindDlg, IDOK);
        HWND hCancel = GetDlgItem(g_hFindDlg, IDCANCEL);
        RECT rcNext = {0};
        RECT rcCancel = {0};
        if (hFindNext)
        {
            GetWindowRect(hFindNext, &rcNext);
            MapWindowPoints(HWND_DESKTOP, g_hFindDlg, (POINT *)&rcNext, 2);
        }
        if (hCancel)
        {
            GetWindowRect(hCancel, &rcCancel);
            MapWindowPoints(HWND_DESKTOP, g_hFindDlg, (POINT *)&rcCancel, 2);
        }

        bool hasCancel = hCancel != NULL;
        RECT base = hasCancel ? rcCancel : rcNext;
        int btnWidth = base.right - base.left;
        int btnHeight = base.bottom - base.top;
        int margin = 8;
        int x = base.left;
        int y = max(rcNext.bottom, rcCancel.bottom) + margin;

        CreateWindowExW(0, L"BUTTON", L"Find All",
                        WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,
                        x, y, btnWidth, btnHeight,
                        g_hFindDlg, (HMENU)(INT_PTR)IDC_FIND_ALL, GetModuleHandleW(NULL), NULL);
    }
}

static void ToggleWordWrap(HWND hwnd)
{
    g_wordWrap = !g_wordWrap;
    RecreateEdit(hwnd);
    UpdateWrapMenu();
}

static void ChooseFontAndApply(HWND hwnd)
{
    CHOOSEFONTW cf = {0};
    cf.lStructSize = sizeof(cf);
    cf.hwndOwner = hwnd;
    cf.lpLogFont = &g_logFont;
    cf.Flags = CF_SCREENFONTS | CF_INITTOLOGFONTSTRUCT | CF_EFFECTS;

    if (ChooseFontW(&cf))
    {
        if (g_hFont)
        {
            DeleteObject(g_hFont);
            g_hFont = NULL;
        }
        g_hFont = CreateFontIndirectW(&g_logFont);
        SetDefaultFont(g_hEdit);
    }
}

static void ShowAbout(HWND hwnd)
{
    WCHAR message[256];
    swprintf_s(message, ARRAYSIZE(message), L"ClassicNotepad v%s\nBuilt with Win32 API in C.\ngithub.com/acidburnmonkey/ClassicNotepad", APP_VERSION);
    MessageBoxW(hwnd, message, L"About", MB_OK | MB_ICONINFORMATION);
}

static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    if (msg == g_msgFindReplace)
    {
        LPFINDREPLACEW pfr = (LPFINDREPLACEW)lParam;
        if (pfr->Flags & FR_DIALOGTERM)
        {
            g_hFindDlg = NULL;
        }
        else
        {
            if (pfr->lpstrFindWhat)
            {
                CopyStringSafe(g_findText, ARRAYSIZE(g_findText), pfr->lpstrFindWhat);
            }
            if (pfr->lpstrReplaceWith)
            {
                CopyStringSafe(g_replaceText, ARRAYSIZE(g_replaceText), pfr->lpstrReplaceWith);
            }
            g_findFlags = pfr->Flags & (FR_DOWN | FR_MATCHCASE);
            EnsureFindAllButton();
            if (pfr->Flags & FR_FINDNEXT)
            {
                if (FindAndSelectText(hwnd, g_findFlags, true))
                {
                    SetFocus(g_hEdit);
                }
            }
            else if (pfr->Flags & FR_REPLACE)
            {
                HandleReplaceCurrent(hwnd);
                SetFocus(g_hEdit);
            }
            else if (pfr->Flags & FR_REPLACEALL)
            {
                ReplaceAllOccurrences(hwnd);
                SetFocus(g_hEdit);
            }
        }
        return 0;
    }

    switch (msg)
    {
    case WM_CREATE:
        g_hMenu = BuildMenu();
        SetMenu(hwnd, g_hMenu);
        RecreateEdit(hwnd);
        UpdateTitle(hwnd);
        return 0;
    case WM_SIZE:
        ResizeEdit(hwnd);
        return 0;
    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case IDM_FILE_NEW:
            DoFileNew(hwnd);
            break;
        case IDM_FILE_OPEN:
            DoFileOpen(hwnd);
            break;
        case IDM_FILE_SAVE:
            DoFileSave(hwnd);
            break;
        case IDM_FILE_SAVE_AS:
            DoFileSaveAs(hwnd);
            break;
        case IDM_FILE_EXIT:
            PostMessageW(hwnd, WM_CLOSE, 0, 0);
            break;
        case IDM_EDIT_UNDO:
            SendMessageW(g_hEdit, WM_UNDO, 0, 0);
            break;
        case IDM_EDIT_CUT:
            SendMessageW(g_hEdit, WM_CUT, 0, 0);
            break;
        case IDM_EDIT_COPY:
            SendMessageW(g_hEdit, WM_COPY, 0, 0);
            break;
        case IDM_EDIT_PASTE:
            SendMessageW(g_hEdit, WM_PASTE, 0, 0);
            break;
        case IDM_EDIT_DELETE:
            SendMessageW(g_hEdit, WM_CLEAR, 0, 0);
            break;
        case IDM_EDIT_FIND:
            ShowFindOrReplaceDialog(hwnd, false);
            break;
        case IDM_EDIT_FIND_NEXT:
            if (!FindAndSelectText(hwnd, g_findFlags, true))
            {
                ShowFindOrReplaceDialog(hwnd, false);
            }
            break;
        case IDM_EDIT_REPLACE:
            ShowFindOrReplaceDialog(hwnd, true);
            break;
        case IDM_EDIT_SELECT:
            SendMessageW(g_hEdit, EM_SETSEL, 0, -1);
            break;
        case IDM_EDIT_TIME:
            InsertTimeDate();
            break;
        case IDM_FORMAT_WRAP:
            ToggleWordWrap(hwnd);
            break;
        case IDM_FORMAT_FONT:
            ChooseFontAndApply(hwnd);
            break;
        case IDM_HELP_ABOUT:
            ShowAbout(hwnd);
            break;
        }
        return 0;
    case WM_SETFOCUS:
        SetFocus(g_hEdit);
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProcW(hwnd, msg, wParam, lParam);
}

int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
{
    (void)hPrevInstance;

    {
        HMODULE hUser = GetModuleHandleW(L"user32.dll");
        if (hUser)
        {
            BOOL(WINAPI *pSetProcessDPIAware)(void) = (BOOL(WINAPI *)(void))GetProcAddress(hUser, "SetProcessDPIAware");
            if (pSetProcessDPIAware)
            {
                pSetProcessDPIAware();
            }
        }
    }

    g_msgFindReplace = RegisterWindowMessageW(FINDMSGSTRING);

    ACCEL accels[] = {
        {FVIRTKEY | FCONTROL, 'N', IDM_FILE_NEW},
        {FVIRTKEY | FCONTROL, 'O', IDM_FILE_OPEN},
        {FVIRTKEY | FCONTROL, 'S', IDM_FILE_SAVE},
        {FVIRTKEY | FCONTROL, 'A', IDM_EDIT_SELECT},
        {FVIRTKEY | FCONTROL, 'Z', IDM_EDIT_UNDO},
        {FVIRTKEY | FCONTROL, 'X', IDM_EDIT_CUT},
        {FVIRTKEY | FCONTROL, 'C', IDM_EDIT_COPY},
        {FVIRTKEY | FCONTROL, 'V', IDM_EDIT_PASTE},
        {FVIRTKEY | FCONTROL, 'F', IDM_EDIT_FIND},
        {FVIRTKEY, VK_F3, IDM_EDIT_FIND_NEXT},
        {FVIRTKEY | FCONTROL, 'H', IDM_EDIT_REPLACE},
        {FVIRTKEY, VK_F5, IDM_EDIT_TIME},
    };
    g_hAccel = CreateAcceleratorTableW(accels, sizeof(accels) / sizeof(accels[0]));

    WNDCLASSEXW wc = {0};
    wc.cbSize = sizeof(wc);
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInstance;
    HICON hIconLarge = (HICON)LoadImageW(hInstance, MAKEINTRESOURCEW(IDI_APP), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE);
    HICON hIconSmall = (HICON)LoadImageW(hInstance, MAKEINTRESOURCEW(IDI_APP), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), 0);
    if (!hIconLarge)
    {
        hIconLarge = LoadIcon(NULL, IDI_APPLICATION);
    }
    if (!hIconSmall)
    {
        hIconSmall = LoadIcon(NULL, IDI_APPLICATION);
    }

    wc.hIcon = hIconLarge;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.lpszClassName = L"ClassicNotepadClass";
    wc.hIconSm = hIconSmall;

    if (!RegisterClassExW(&wc))
    {
        MessageBoxW(NULL, L"Failed to register window class.", L"Error", MB_ICONERROR | MB_OK);
        return -1;
    }

    int screenW = GetSystemMetrics(SM_CXSCREEN);
    int screenH = GetSystemMetrics(SM_CYSCREEN);
    int winWidth = (int)(screenW * 0.6);
    int winHeight = (int)(screenH * 0.6);
    int posX = (screenW - winWidth) / 2;
    int posY = (screenH - winHeight) / 2;

    HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"ClassicNotepad", WS_OVERLAPPEDWINDOW,
                                posX, posY, winWidth, winHeight, NULL, NULL, hInstance, NULL);

    if (!hwnd)
    {
        MessageBoxW(NULL, L"Failed to create window.", L"Error", MB_ICONERROR | MB_OK);
        return -1;
    }

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    if (lpCmdLine && lpCmdLine[0])
    {
        int argc;
        LPWSTR *argv = CommandLineToArgvW(lpCmdLine, &argc);
        if (argv && argc >= 2)
        {
            LoadFileToEdit(hwnd, argv[1]);
        }
        if (argv) LocalFree(argv);
    }

    MSG msg;
    while (GetMessageW(&msg, NULL, 0, 0) > 0)
    {
        if (g_hFindDlg && IsDialogMessageW(g_hFindDlg, &msg))
        {
            continue;
        }
        if (!TranslateAcceleratorW(hwnd, g_hAccel, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
    }

    return (int)msg.wParam;
}

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    (void)lpCmdLine;
    return wWinMain(hInstance, hPrevInstance, GetCommandLineW(), nCmdShow);
}
