/*
 * Process Hacker -
 *   hex editor control
 *
 * Copyright (C) 2010-2015 wj32
 *
 * This file is part of Process Hacker.
 *
 * Process Hacker 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 3 of the License, or
 * (at your option) any later version.
 *
 * Process Hacker 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 Process Hacker.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <ph.h>
#include <guisup.h>
#include <hexedit.h>
#include <hexeditp.h>

// Code originally from http://www.codeguru.com/Cpp/controls/editctrl/article.php/c539

BOOLEAN PhHexEditInitialization(
    VOID
    )
{
    WNDCLASSEX c = { sizeof(c) };

    c.style = CS_GLOBALCLASS;
    c.lpfnWndProc = PhpHexEditWndProc;
    c.cbClsExtra = 0;
    c.cbWndExtra = sizeof(PVOID);
    c.hInstance = PhLibImageBase;
    c.hIcon = NULL;
    c.hCursor = LoadCursor(NULL, IDC_ARROW);
    c.hbrBackground = NULL;
    c.lpszMenuName = NULL;
    c.lpszClassName = PH_HEXEDIT_CLASSNAME;
    c.hIconSm = NULL;

    if (!RegisterClassEx(&c))
        return FALSE;

    return TRUE;
}

VOID PhpCreateHexEditContext(
    _Out_ PPHP_HEXEDIT_CONTEXT *Context
    )
{
    PPHP_HEXEDIT_CONTEXT context;

    context = PhAllocate(sizeof(PHP_HEXEDIT_CONTEXT));
    memset(context, 0, sizeof(PHP_HEXEDIT_CONTEXT)); // important, set NullWidth to 0

    context->Data = NULL;
    context->Length = 0;
    context->TopIndex = 0;
    context->BytesPerRow = 16;
    context->LinesPerPage = 1;

    context->ShowHex = TRUE;
    context->ShowAscii = TRUE;
    context->ShowAddress = TRUE;
    context->AddressIsWide = TRUE;
    context->AllowLengthChange = FALSE;

    context->AddressOffset = 0;
    context->HexOffset = 0;
    context->AsciiOffset = 0;

    context->Update = TRUE;
    context->NoAddressChange = FALSE;
    context->CurrentMode = EDIT_NONE;

    context->EditPosition.x = 0;
    context->EditPosition.y = 0;
    context->CurrentAddress = 0;
    context->HalfPage = TRUE;

    context->SelStart = -1;
    context->SelEnd = -1;

    *Context = context;
}

VOID PhpFreeHexEditContext(
    _In_ _Post_invalid_ PPHP_HEXEDIT_CONTEXT Context
    )
{
    if (!Context->UserBuffer && Context->Data) PhFree(Context->Data);
    if (Context->CharBuffer) PhFree(Context->CharBuffer);
    if (Context->Font) DeleteObject(Context->Font);
    PhFree(Context);
}

LRESULT CALLBACK PhpHexEditWndProc(
    _In_ HWND hwnd,
    _In_ UINT uMsg,
    _In_ WPARAM wParam,
    _In_ LPARAM lParam
    )
{
    PPHP_HEXEDIT_CONTEXT context;

    context = (PPHP_HEXEDIT_CONTEXT)GetWindowLongPtr(hwnd, 0);

    if (uMsg == WM_CREATE)
    {
        PhpCreateHexEditContext(&context);
        SetWindowLongPtr(hwnd, 0, (LONG_PTR)context);
    }

    if (!context)
        return DefWindowProc(hwnd, uMsg, wParam, lParam);

    switch (uMsg)
    {
    case WM_CREATE:
        {
            context->Font = CreateFont(-(LONG)PhMultiplyDivide(12, PhGlobalDpi, 96), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, L"Courier New");
        }
        break;
    case WM_DESTROY:
        {
            PhpFreeHexEditContext(context);
        }
        break;
    case WM_PAINT:
        {
            PAINTSTRUCT paintStruct;
            HDC hdc;

            if (hdc = BeginPaint(hwnd, &paintStruct))
            {
                PhpHexEditOnPaint(hwnd, context, &paintStruct, hdc);
                EndPaint(hwnd, &paintStruct);
            }
        }
        break;
    case WM_SIZE:
        {
            PhpHexEditUpdateMetrics(hwnd, context, FALSE, NULL);
        }
        break;
    case WM_SETFOCUS:
        {
            if (context->Data && !PhpHexEditHasSelected(context))
            {
                if (context->EditPosition.x == 0 && context->ShowAddress)
                    PhpHexEditCreateAddressCaret(hwnd, context);
                else
                    PhpHexEditCreateEditCaret(hwnd, context);

                SetCaretPos(context->EditPosition.x, context->EditPosition.y);
                ShowCaret(hwnd);
            }
        }
        break;
    case WM_KILLFOCUS:
        {
            DestroyCaret();
        }
        break;
    case WM_VSCROLL:
        {
            SHORT scrollRequest = LOWORD(wParam);
            LONG currentPosition;
            LONG originalTopIndex;
            SCROLLINFO scrollInfo = { sizeof(scrollInfo) };

            originalTopIndex = context->TopIndex;

            scrollInfo.fMask = SIF_TRACKPOS;
            GetScrollInfo(hwnd, SB_VERT, &scrollInfo);
            currentPosition = scrollInfo.nTrackPos;

            if (context->Data)
            {
                LONG mult;

                mult = context->LinesPerPage * context->BytesPerRow;

                switch (scrollRequest)
                {
                case SB_LINEDOWN:
                    if (context->TopIndex < context->Length - mult)
                    {
                        context->TopIndex += context->BytesPerRow;
                        REDRAW_WINDOW(hwnd);
                    }
                    break;
                case SB_LINEUP:
                    if (context->TopIndex >= context->BytesPerRow)
                    {
                        context->TopIndex -= context->BytesPerRow;
                        REDRAW_WINDOW(hwnd);
                    }
                    break;
                case SB_PAGEDOWN:
                    if (context->TopIndex < context->Length - mult)
                    {
                        context->TopIndex += mult;

                        if (context->TopIndex > context->Length - mult)
                            context->TopIndex = context->Length - mult;

                        REDRAW_WINDOW(hwnd);
                    }
                    break;
                case SB_PAGEUP:
                    if (context->TopIndex > 0)
                    {
                        context->TopIndex -= mult;

                        if (context->TopIndex < 0)
                            context->TopIndex = 0;

                        REDRAW_WINDOW(hwnd);
                    }
                    break;
                case SB_THUMBTRACK:
                    context->TopIndex = currentPosition * context->BytesPerRow;
                    REDRAW_WINDOW(hwnd);
                    break;
                }

                SetScrollPos(hwnd, SB_VERT, context->TopIndex / context->BytesPerRow, TRUE);

                if (!context->NoAddressChange && FALSE) // this behaviour sucks, so just leave it out
                    context->CurrentAddress += context->TopIndex - originalTopIndex;

                PhpHexEditRepositionCaret(hwnd, context, context->CurrentAddress);
            }
        }
        break;
    case WM_MOUSEWHEEL:
        {
            SHORT wheelDelta = GET_WHEEL_DELTA_WPARAM(wParam);

            if (context->Data)
            {
                ULONG wheelScrollLines;

                if (!SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &wheelScrollLines, 0))
                    wheelScrollLines = 3;

                context->TopIndex += context->BytesPerRow * (LONG)wheelScrollLines * -wheelDelta / WHEEL_DELTA;

                if (context->TopIndex < 0)
                    context->TopIndex = 0;

                if (context->Length >= context->LinesPerPage * context->BytesPerRow)
                {
                    if (context->TopIndex > context->Length - context->LinesPerPage * context->BytesPerRow)
                        context->TopIndex = context->Length - context->LinesPerPage * context->BytesPerRow;
                }

                REDRAW_WINDOW(hwnd);

                SetScrollPos(hwnd, SB_VERT, context->TopIndex / context->BytesPerRow, TRUE);

                PhpHexEditRepositionCaret(hwnd, context, context->CurrentAddress);
            }
        }
        break;
    case WM_GETDLGCODE:
        if (wParam != VK_ESCAPE)
            return DLGC_WANTALLKEYS;
        break;
    case WM_ERASEBKGND:
        return 1;
    case WM_LBUTTONDOWN:
        {
            ULONG flags = (ULONG)wParam;
            POINT cursorPos;

            cursorPos.x = (LONG)(SHORT)LOWORD(lParam);
            cursorPos.y = (LONG)(SHORT)HIWORD(lParam);

            SetFocus(hwnd);

            if (context->Data)
            {
                POINT point;

                if (wParam & MK_SHIFT)
                    context->SelStart = context->CurrentAddress;

                PhpHexEditCalculatePosition(hwnd, context, cursorPos.x, cursorPos.y, &point);

                if (point.x > -1)
                {
                    context->EditPosition = point;

                    point.x *= context->NullWidth;
                    point.y *= context->LineHeight;

                    if (point.x == 0 && context->ShowAddress)
                        PhpHexEditCreateAddressCaret(hwnd, context);
                    else
                        PhpHexEditCreateEditCaret(hwnd, context);

                    SetCaretPos(point.x, point.y);

                    if (flags & MK_SHIFT)
                    {
                        context->SelEnd = context->CurrentAddress;

                        if (context->CurrentMode == EDIT_HIGH || context->CurrentMode == EDIT_LOW)
                            context->SelEnd++;

                        REDRAW_WINDOW(hwnd);
                    }
                }

                if (!(flags & MK_SHIFT))
                {
                    if (DragDetect(hwnd, cursorPos))
                    {
                        context->SelStart = context->CurrentAddress;
                        context->SelEnd = context->SelStart;
                        SetCapture(hwnd);
                        context->HasCapture = TRUE;
                    }
                    else
                    {
                        BOOLEAN selected;

                        selected = context->SelStart != -1;
                        context->SelStart = -1;
                        context->SelEnd = -1;

                        if (selected)
                            REDRAW_WINDOW(hwnd);
                    }
                }

                if (!PhpHexEditHasSelected(context))
                    ShowCaret(hwnd);
            }
        }
        break;
    case WM_LBUTTONUP:
        {
            if (context->HasCapture && PhpHexEditHasSelected(context))
                ReleaseCapture();

            context->HasCapture = FALSE;
        }
        break;
    case WM_MOUSEMOVE:
        {
            ULONG flags = (ULONG)wParam;
            POINT cursorPos;

            cursorPos.x = (LONG)(SHORT)LOWORD(lParam);
            cursorPos.y = (LONG)(SHORT)HIWORD(lParam);

            if (
                context->Data &&
                context->HasCapture &&
                context->SelStart != -1
                )
            {
                RECT rect;
                POINT point;
                ULONG oldSelEnd;

                // User is dragging.

                GetClientRect(hwnd, &rect);

                if (!PtInRect(&rect, cursorPos))
                {
                    if (cursorPos.y < 0)
                    {
                        SendMessage(hwnd, WM_VSCROLL, SB_LINEUP, 0);
                        cursorPos.y = 0;
                    }
                    else if (cursorPos.y > rect.bottom)
                    {
                        SendMessage(hwnd, WM_VSCROLL, SB_LINEDOWN, 0);
                        cursorPos.y = rect.bottom - 1;
                    }
                }

                oldSelEnd = context->SelEnd;
                PhpHexEditCalculatePosition(hwnd, context, cursorPos.x, cursorPos.y, &point);

                if (point.x > -1)
                {
                    context->SelEnd = context->CurrentAddress;

                    if (context->CurrentMode == EDIT_HIGH || context->CurrentMode == EDIT_LOW)
                        context->SelEnd++;
                }

                if (PhpHexEditHasSelected(context))
                    DestroyCaret();

                if (context->SelEnd != oldSelEnd)
                    REDRAW_WINDOW(hwnd);
            }
        }
        break;
    case WM_CHAR:
        {
            ULONG c = (ULONG)wParam;

            if (!context->Data)
                goto DefaultHandler;
            if (c == '\t')
                goto DefaultHandler;

            if (GetKeyState(VK_CONTROL) < 0)
            {
                switch (c)
                {
                case 0x3:
                    if (PhpHexEditHasSelected(context))
                        PhpHexEditCopyEdit(hwnd, context);
                    goto DefaultHandler;
                case 0x16:
                    PhpHexEditPasteEdit(hwnd, context);
                    goto DefaultHandler;
                case 0x18:
                    if (PhpHexEditHasSelected(context))
                        PhpHexEditCutEdit(hwnd, context);
                    goto DefaultHandler;
                case 0x1a:
                    PhpHexEditUndoEdit(hwnd, context);
                    goto DefaultHandler;
                }
            }

            // Disallow editing beyond the end of the data.
            if (context->CurrentAddress >= context->Length)
                goto DefaultHandler;

            if (c == 0x8)
            {
                if (context->CurrentAddress != 0)
                {
                    context->CurrentAddress--;
                    PhpHexEditSelDelete(hwnd, context, context->CurrentAddress, context->CurrentAddress + 1);
                    PhpHexEditRepositionCaret(hwnd, context, context->CurrentAddress);
                    REDRAW_WINDOW(hwnd);
                }

                goto DefaultHandler;
            }

            PhpHexEditSetSel(hwnd, context, -1, -1);

            switch (context->CurrentMode)
            {
            case EDIT_NONE:
                goto DefaultHandler;
            case EDIT_HIGH:
            case EDIT_LOW:
                if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))
                {
                    ULONG b = c - '0';

                    if (b > 9)
                        b = 10 + c - 'a';

                    if (context->CurrentMode == EDIT_HIGH)
                    {
                        context->Data[context->CurrentAddress] =
                            (UCHAR)((context->Data[context->CurrentAddress] & 0x0f) | (b << 4));
                    }
                    else
                    {
                        context->Data[context->CurrentAddress] =
                            (UCHAR)((context->Data[context->CurrentAddress] & 0xf0) | b);
                    }

                    PhpHexEditMove(hwnd, context, 1, 0);
                }
                break;
            case EDIT_ASCII:
                context->Data[context->CurrentAddress] = (UCHAR)c;
                PhpHexEditMove(hwnd, context, 1, 0);
                break;
            }

            REDRAW_WINDOW(hwnd);
        }
        break;
    case WM_KEYDOWN:
        {
            ULONG vk = (ULONG)wParam;
            BOOLEAN shift = GetKeyState(VK_SHIFT) < 0;
            BOOLEAN oldNoAddressChange = context->NoAddressChange;
            BOOLEAN noScrollIntoView = FALSE;

            context->NoAddressChange = TRUE;

            switch (vk)
            {
            case VK_DOWN:
                if (context->CurrentMode != EDIT_NONE)
                {
                    if (shift)
                    {
                        if (!PhpHexEditHasSelected(context))
                            context->SelStart = context->CurrentAddress;

                        PhpHexEditMove(hwnd, context, 0, 1);
                        context->SelEnd = context->CurrentAddress;

                        if (context->CurrentMode == EDIT_HIGH || context->CurrentMode == EDIT_LOW)
                            context->SelEnd++;

                        REDRAW_WINDOW(hwnd);
                        break;
                    }
                    else
                    {
                        PhpHexEditSetSel(hwnd, context, -1, -1);
                    }

                    PhpHexEditMove(hwnd, context, 0, 1);
                    noScrollIntoView = TRUE;
                }
                else
                {
                    PhpHexEditMove(hwnd, context, 0, 1);
                }
                break;
            case VK_UP:
                if (context->CurrentMode != EDIT_NONE)
                {
                    if (shift)
                    {
                        if (!PhpHexEditHasSelected(context))
                            context->SelStart = context->CurrentAddress;

                        PhpHexEditMove(hwnd, context, 0, -1);
                        context->SelEnd = context->CurrentAddress;

                        REDRAW_WINDOW(hwnd);
                        break;
                    }
                    else
                    {
                        PhpHexEditSetSel(hwnd, context, -1, -1);
                    }

                    PhpHexEditMove(hwnd, context, 0, -1);
                    noScrollIntoView = TRUE;
                }
                else
                {
                    PhpHexEditMove(hwnd, context, 0, -1);
                }
                break;
            case VK_LEFT:
                if (context->CurrentMode != EDIT_NONE)
                {
                    if (shift)
                    {
                        if (!PhpHexEditHasSelected(context))
                            context->SelStart = context->CurrentAddress;

                        PhpHexEditMove(hwnd, context, -1, 0);
                        context->SelEnd = context->CurrentAddress;

                        REDRAW_WINDOW(hwnd);
                        break;
                    }
                    else
                    {
                        PhpHexEditSetSel(hwnd, context, -1, -1);
                    }

                    PhpHexEditMove(hwnd, context, -1, 0);
                    noScrollIntoView = TRUE;
                }
                break;
            case VK_RIGHT:
                if (context->CurrentMode != EDIT_NONE)
                {
                    if (shift)
                    {
                        if (!PhpHexEditHasSelected(context))
                            context->SelStart = context->CurrentAddress;

                        PhpHexEditMove(hwnd, context, 1, 0);
                        context->SelEnd = context->CurrentAddress;

                        if (context->CurrentMode == EDIT_HIGH || context->CurrentMode == EDIT_LOW)
                            context->SelEnd++;

                        REDRAW_WINDOW(hwnd);
                        break;
                    }
                    else
                    {
                        PhpHexEditSetSel(hwnd, context, -1, -1);
                    }

                    PhpHexEditMove(hwnd, context, 1, 0);
                    noScrollIntoView = TRUE;
                }
                break;
            case VK_PRIOR:
                if (shift)
                {
                    if (!PhpHexEditHasSelected(context))
                        context->SelStart = context->CurrentAddress;

                    SendMessage(hwnd, WM_VSCROLL, SB_PAGEUP, 0);
                    PhpHexEditMove(hwnd, context, 0, 0);
                    context->SelEnd = context->CurrentAddress;

                    REDRAW_WINDOW(hwnd);
                    break;
                }
                else
                {
                    PhpHexEditSetSel(hwnd, context, -1, -1);
                }

                SendMessage(hwnd, WM_VSCROLL, SB_PAGEUP, 0);
                PhpHexEditMove(hwnd, context, 0, 0);
                noScrollIntoView = TRUE;
                break;
            case VK_NEXT:
                if (shift)
                {
                    if (!PhpHexEditHasSelected(context))
                        context->SelStart = context->CurrentAddress;

                    SendMessage(hwnd, WM_VSCROLL, SB_PAGEDOWN, 0);
                    PhpHexEditMove(hwnd, context, 0, 0);
                    context->SelEnd = context->CurrentAddress;

                    REDRAW_WINDOW(hwnd);
                    break;
                }
                else
                {
                    PhpHexEditSetSel(hwnd, context, -1, -1);
                }

                SendMessage(hwnd, WM_VSCROLL, SB_PAGEDOWN, 0);
                PhpHexEditMove(hwnd, context, 0, 0);
                noScrollIntoView = TRUE;
                break;
            case VK_HOME:
                if (shift)
                {
                    if (!PhpHexEditHasSelected(context))
                        context->SelStart = context->CurrentAddress;

                    if (GetKeyState(VK_CONTROL) < 0)
                    {
                        SendMessage(hwnd, WM_VSCROLL, SB_THUMBTRACK, 0);
                    }
                    else
                    {
                        // Round down.
                        context->CurrentAddress /= context->BytesPerRow;
                        context->CurrentAddress *= context->BytesPerRow;
                    }

                    PhpHexEditMove(hwnd, context, 0, 0);
                    context->SelEnd = context->CurrentAddress;

                    REDRAW_WINDOW(hwnd);
                    break;
                }
                else
                {
                    PhpHexEditSetSel(hwnd, context, -1, -1);
                }

                if (GetKeyState(VK_CONTROL) < 0)
                {
                    SendMessage(hwnd, WM_VSCROLL, SB_THUMBTRACK, 0);
                    context->CurrentAddress = 0;
                }
                else
                {
                    // Round down.
                    context->CurrentAddress /= context->BytesPerRow;
                    context->CurrentAddress *= context->BytesPerRow;
                }

                PhpHexEditMove(hwnd, context, 0, 0);
                noScrollIntoView = TRUE;

                break;
            case VK_END:
                if (shift)
                {
                    if (!PhpHexEditHasSelected(context))
                        context->SelStart = context->CurrentAddress;

                    if (GetKeyState(VK_CONTROL) < 0)
                    {
                        context->CurrentAddress = context->Length - 1;
                        SendMessage(hwnd, WM_VSCROLL,
                            MAKEWPARAM(SB_THUMBTRACK, ((context->Length + (context->BytesPerRow / 2)) / context->BytesPerRow) - context->LinesPerPage),
                            0);
                    }
                    else
                    {
                        context->CurrentAddress /= context->BytesPerRow;
                        context->CurrentAddress *= context->BytesPerRow;
                        context->CurrentAddress += context->BytesPerRow - 1;

                        if (context->CurrentAddress > context->Length)
                            context->CurrentAddress = context->Length - 1;
                    }

                    PhpHexEditMove(hwnd, context, 0, 0);
                    context->SelEnd = context->CurrentAddress;

                    REDRAW_WINDOW(hwnd);
                    break;
                }
                else
                {
                    PhpHexEditSetSel(hwnd, context, -1, -1);
                }

                if (GetKeyState(VK_CONTROL) < 0)
                {
                    context->CurrentAddress = context->Length - 1;

                    if (context->HalfPage)
                    {
                        SendMessage(hwnd, WM_VSCROLL, 0, 0);
                    }
                    else
                    {
                        SendMessage(hwnd, WM_VSCROLL,
                            MAKEWPARAM(SB_THUMBTRACK, ((context->Length + (context->BytesPerRow / 2)) / context->BytesPerRow) - context->LinesPerPage),
                            0);
                    }
                }
                else
                {
                    context->CurrentAddress /= context->BytesPerRow;
                    context->CurrentAddress *= context->BytesPerRow;
                    context->CurrentAddress += context->BytesPerRow - 1;

                    if (context->CurrentAddress > context->Length)
                        context->CurrentAddress = context->Length - 1;
                }

                PhpHexEditMove(hwnd, context, 0, 0);
                noScrollIntoView = TRUE;

                break;
            case VK_INSERT:
                PhpHexEditSelInsert(hwnd, context, context->CurrentAddress,
                    max(1, context->SelEnd - context->SelStart));
                REDRAW_WINDOW(hwnd);
                break;
            case VK_DELETE:
                if (PhpHexEditHasSelected(context))
                {
                    PhpHexEditClearEdit(hwnd, context);
                }
                else
                {
                   PhpHexEditSelDelete(hwnd, context, context->CurrentAddress, context->CurrentAddress + 1);
                   REDRAW_WINDOW(hwnd);
                }
                break;
            case '\t':
                switch (context->CurrentMode)
                {
                case EDIT_NONE:
                    context->CurrentMode = EDIT_HIGH;
                    break;
                case EDIT_HIGH:
                case EDIT_LOW:
                    context->CurrentMode = EDIT_ASCII;
                    break;
                case EDIT_ASCII:
                    context->CurrentMode = EDIT_HIGH;
                    break;
                }

                PhpHexEditMove(hwnd, context, 0, 0);

                break;
            }

            // Scroll into view if not in view.
            if (
                !noScrollIntoView &&
                (context->CurrentAddress < context->TopIndex ||
                context->CurrentAddress >= context->TopIndex + context->LinesPerPage * context->BytesPerRow)
                )
            {
                PhpHexEditScrollTo(hwnd, context, context->CurrentAddress);
            }

            context->NoAddressChange = oldNoAddressChange;
        }
        break;
    case HEM_SETBUFFER:
        {
            PhpHexEditSetBuffer(hwnd, context, (PUCHAR)lParam, (ULONG)wParam);
        }
        return TRUE;
    case HEM_SETDATA:
        {
            PhpHexEditSetData(hwnd, context, (PUCHAR)lParam, (ULONG)wParam);
        }
        return TRUE;
    case HEM_GETBUFFER:
        {
            PULONG length = (PULONG)wParam;

            if (length)
                *length = context->Length;

            return (LPARAM)context->Data;
        }
    case HEM_SETSEL:
        {
            LONG selStart = (LONG)wParam;
            LONG selEnd = (LONG)lParam;

            if (selStart <= 0)
                return FALSE;
            if (selEnd > context->Length)
                return FALSE;

            PhpHexEditScrollTo(hwnd, context, selStart);
            PhpHexEditSetSel(hwnd, context, selStart, selEnd);
            PhpHexEditRepositionCaret(hwnd, context, selStart);
            REDRAW_WINDOW(hwnd);
        }
        return TRUE;
    case HEM_SETEDITMODE:
        {
            context->CurrentMode = (LONG)wParam;
            REDRAW_WINDOW(hwnd);
        }
        return TRUE;
    case HEM_SETBYTESPERROW:
        {
            LONG bytesPerRow = (LONG)wParam;

            if (bytesPerRow >= 4)
            {
                context->BytesPerRow = bytesPerRow;
                PhpHexEditUpdateMetrics(hwnd, context, TRUE, NULL);
                PhpHexEditUpdateScrollbars(hwnd, context);
                PhpHexEditScrollTo(hwnd, context, context->CurrentAddress);
                PhpHexEditRepositionCaret(hwnd, context, context->CurrentAddress);
                REDRAW_WINDOW(hwnd);
            }
        }
        return TRUE;
    }

DefaultHandler:
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

FORCEINLINE VOID PhpPrintHex(
    _In_ HDC hdc,
    _In_ PPHP_HEXEDIT_CONTEXT Context,
    _Inout_ PWCHAR Buffer,
    _In_ UCHAR Byte,
    _Inout_ PLONG X,
    _Inout_ PLONG Y,
    _Inout_ PULONG N
    )
{
    PWCHAR p = Buffer;

    TO_HEX(p, Byte);
    *p++ = ' ';
    TextOut(hdc, *X, *Y, Buffer, 3);
    *X += Context->NullWidth * 3;
    (*N)++;

    if (*N == Context->BytesPerRow)
    {
        *N = 0;
        *X = Context->HexOffset;
        *Y += Context->LineHeight;
    }
}

FORCEINLINE VOID PhpPrintAscii(
    _In_ HDC hdc,
    _In_ PPHP_HEXEDIT_CONTEXT Context,
    _In_ UCHAR Byte,
    _Inout_ PLONG X,
    _Inout_ PLONG Y,
    _Inout_ PULONG N
    )
{
    WCHAR c;

    c = IS_PRINTABLE(Byte) ? Byte : '.';
    TextOut(hdc, *X, *Y, &c, 1);
    *X += Context->NullWidth;
    (*N)++;

    if (*N == Context->BytesPerRow)
    {
        *N = 0;
        *X = Context->AsciiOffset;
        *Y += Context->LineHeight;
    }
}

FORCEINLINE COLORREF GetLighterHighlightColor(
    VOID
    )
{
    COLORREF color;
    UCHAR r;
    UCHAR g;
    UCHAR b;

    color = GetSysColor(COLOR_HIGHLIGHT);
    r = (UCHAR)color;
    g = (UCHAR)(color >> 8);
    b = (UCHAR)(color >> 16);

    if (r <= 255 - 64)
        r += 64;
    else
        r = 255;

    if (g <= 255 - 64)
        g += 64;
    else
        g = 255;

    if (b <= 255 - 64)
        b += 64;
    else
        b = 255;

    return RGB(r, g, b);
}

VOID PhpHexEditUpdateMetrics(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context,
    _In_ BOOLEAN UpdateLineHeight,
    _In_opt_ HDC hdc
    )
{
    BOOLEAN freeHdc = FALSE;
    RECT clientRect;
    SIZE size;

    if (!hdc && UpdateLineHeight)
    {
        hdc = CreateCompatibleDC(hdc);
        SelectObject(hdc, Context->Font);
        freeHdc = TRUE;
    }

    GetClientRect(hwnd, &clientRect);

    if (UpdateLineHeight)
    {
        GetCharWidth(hdc, '0', '0', &Context->NullWidth);
        GetTextExtentPoint32(hdc, L"0", 1, &size);
        Context->LineHeight = size.cy;
    }

    Context->HexOffset = Context->ShowAddress ? (Context->AddressIsWide ? Context->NullWidth * 9 : Context->NullWidth * 5) : 0;
    Context->AsciiOffset = Context->HexOffset + (Context->ShowHex ? (Context->BytesPerRow * 3 * Context->NullWidth) : 0);

    if (Context->LineHeight != 0)
    {
        Context->LinesPerPage = clientRect.bottom / Context->LineHeight;
        Context->HalfPage = FALSE;

        if (Context->LinesPerPage * Context->BytesPerRow > Context->Length)
        {
            Context->LinesPerPage = (Context->Length + Context->BytesPerRow / 2) / Context->BytesPerRow;

            if (Context->Length % Context->BytesPerRow != 0)
            {
                Context->HalfPage = TRUE;
                Context->LinesPerPage++;
            }
        }
    }

    if (freeHdc && hdc)
        DeleteDC(hdc);
}

VOID PhpHexEditOnPaint(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context,
    _In_ PAINTSTRUCT *PaintStruct,
    _In_ HDC hdc
    )
{
    RECT clientRect;
    HDC bufferDc;
    HBITMAP bufferBitmap;
    HBITMAP oldBufferBitmap;
    LONG height;
    LONG x;
    LONG y;
    LONG i;
    ULONG requiredBufferLength;
    PWCHAR buffer;

    GetClientRect(hwnd, &clientRect);

    bufferDc = CreateCompatibleDC(hdc);
    bufferBitmap = CreateCompatibleBitmap(hdc, clientRect.right, clientRect.bottom);
    oldBufferBitmap = SelectObject(bufferDc, bufferBitmap);

    SetDCBrushColor(bufferDc, GetSysColor(COLOR_WINDOW));
    FillRect(bufferDc, &clientRect, GetStockObject(DC_BRUSH));
    SelectObject(bufferDc, Context->Font);
    SetBoundsRect(bufferDc, &clientRect, DCB_DISABLE);

    requiredBufferLength = (max(8, Context->BytesPerRow * 3) + 1) * sizeof(WCHAR);

    if (Context->CharBufferLength < requiredBufferLength)
    {
        if (Context->CharBuffer)
            PhFree(Context->CharBuffer);

        Context->CharBuffer = PhAllocate(requiredBufferLength);
        Context->CharBufferLength = requiredBufferLength;
        buffer = Context->CharBuffer;
    }

    buffer = Context->CharBuffer;

    if (Context->Data)
    {
        // Get character dimensions.
        if (Context->Update)
        {
            PhpHexEditUpdateMetrics(hwnd, Context, TRUE, bufferDc);
            Context->Update = FALSE;
            PhpHexEditUpdateScrollbars(hwnd, Context);
        }

        height = (clientRect.bottom + Context->LineHeight - 1) / Context->LineHeight * Context->LineHeight; // round up to height

        if (Context->ShowAddress)
        {
            PH_FORMAT format;
            ULONG w;
            RECT rect;

            PhInitFormatX(&format, 0);
            format.Type |= FormatPadZeros;
            format.Width = Context->AddressIsWide ? 8 : 4;

            w = Context->AddressIsWide ? 8 : 4;

            rect = clientRect;
            rect.left = Context->AddressOffset;
            rect.top = 0;

            for (i = Context->TopIndex; i < Context->Length && rect.top < height; i += Context->BytesPerRow)
            {
                format.u.Int32 = i;
                PhFormatToBuffer(&format, 1, buffer, requiredBufferLength, NULL);
                DrawText(bufferDc, buffer, w, &rect, DT_LEFT | DT_TOP | DT_SINGLELINE | DT_NOPREFIX | DT_NOCLIP);
                rect.top += Context->LineHeight;
            }
        }

        if (Context->ShowHex)
        {
            RECT rect;
            LONG n = 0;

            x = Context->HexOffset;
            y = 0;
            rect = clientRect;
            rect.left = x;
            rect.top = 0;

            if (Context->SelStart != -1)
            {
                COLORREF highlightColor;
                LONG selStart;
                LONG selEnd;

                if (Context->CurrentMode == EDIT_HIGH || Context->CurrentMode == EDIT_LOW)
                    highlightColor = GetSysColor(COLOR_HIGHLIGHT);
                else
                    highlightColor = GetLighterHighlightColor();

                selStart = Context->SelStart;
                selEnd = Context->SelEnd;

                if (selStart > selEnd)
                {
                    ULONG t;

                    t = selEnd;
                    selEnd = selStart;
                    selStart = t;
                }

                if (selStart >= Context->Length)
                    selStart = Context->Length - 1;
                if (selEnd > Context->Length)
                    selEnd = Context->Length;

                // Bytes before the selection

                for (i = Context->TopIndex; i < selStart && y < height; i++)
                {
                    PhpPrintHex(bufferDc, Context, buffer, Context->Data[i], &x, &y, &n);
                }

                // Bytes in the selection

                SetTextColor(bufferDc, GetSysColor(COLOR_HIGHLIGHTTEXT));
                SetBkColor(bufferDc, highlightColor);

                for (; i < selEnd && i < Context->Length && y < height; i++)
                {
                    PhpPrintHex(bufferDc, Context, buffer, Context->Data[i], &x, &y, &n);
                }

                // Bytes after the selection

                SetTextColor(bufferDc, GetSysColor(COLOR_WINDOWTEXT));
                SetBkColor(bufferDc, GetSysColor(COLOR_WINDOW));

                for (; i < Context->Length && y < height; i++)
                {
                    PhpPrintHex(bufferDc, Context, buffer, Context->Data[i], &x, &y, &n);
                }
            }
            else
            {
                i = Context->TopIndex;

                while (i < Context->Length && rect.top < height)
                {
                    PWCHAR p = buffer;

                    for (n = 0; n < Context->BytesPerRow && i < Context->Length; n++)
                    {
                        TO_HEX(p, Context->Data[i]);
                        *p++ = ' ';
                        i++;
                    }

                    while (n < Context->BytesPerRow)
                    {
                        p[0] = ' ';
                        p[1] = ' ';
                        p[2] = ' ';
                        p += 3;
                        n++;
                    }

                    DrawText(bufferDc, buffer, Context->BytesPerRow * 3, &rect, DT_LEFT | DT_TOP | DT_SINGLELINE | DT_NOPREFIX | DT_NOCLIP);
                    rect.top += Context->LineHeight;
                }
            }
        }

        if (Context->ShowAscii)
        {
            RECT rect;
            LONG n = 0;

            x = Context->AsciiOffset;
            y = 0;
            rect = clientRect;
            rect.left = x;
            rect.top = 0;

            if (Context->SelStart != -1)
            {
                COLORREF highlightColor;
                LONG selStart;
                LONG selEnd;

                if (Context->CurrentMode == EDIT_ASCII)
                    highlightColor = GetSysColor(COLOR_HIGHLIGHT);
                else
                    highlightColor = GetLighterHighlightColor();

                selStart = Context->SelStart;
                selEnd = Context->SelEnd;

                if (selStart > selEnd)
                {
                    LONG t;

                    t = selEnd;
                    selEnd = selStart;
                    selStart = t;
                }

                if (selStart >= Context->Length)
                    selStart = Context->Length - 1;
                if (selEnd > Context->Length)
                    selEnd = Context->Length;

                // Bytes before the selection

                for (i = Context->TopIndex; i < selStart && y < height; i++)
                {
                    PhpPrintAscii(bufferDc, Context, Context->Data[i], &x, &y, &n);
                }

                // Bytes in the selection

                SetTextColor(bufferDc, GetSysColor(COLOR_HIGHLIGHTTEXT));
                SetBkColor(bufferDc, highlightColor);

                for (; i < selEnd && i < Context->Length && y < height; i++)
                {
                    PhpPrintAscii(bufferDc, Context, Context->Data[i], &x, &y, &n);
                }

                // Bytes after the selection

                SetTextColor(bufferDc, GetSysColor(COLOR_WINDOWTEXT));
                SetBkColor(bufferDc, GetSysColor(COLOR_WINDOW));

                for (; i < Context->Length && y < height; i++)
                {
                    PhpPrintAscii(bufferDc, Context, Context->Data[i], &x, &y, &n);
                }
            }
            else
            {
                i = Context->TopIndex;

                while (i < Context->Length && rect.top < height)
                {
                    PWCHAR p = buffer;

                    for (n = 0; n < Context->BytesPerRow && i < Context->Length; n++)
                    {
                        *p++ = IS_PRINTABLE(Context->Data[i]) ? Context->Data[i] : '.'; // 1
                        i++;
                    }

                    DrawText(bufferDc, buffer, n, &rect, DT_LEFT | DT_TOP | DT_SINGLELINE | DT_NOPREFIX | DT_NOCLIP);
                    rect.top += Context->LineHeight;
                }
            }
        }
    }

    BitBlt(hdc, 0, 0, clientRect.right, clientRect.bottom, bufferDc, 0, 0, SRCCOPY);
    SelectObject(bufferDc, oldBufferBitmap);
    DeleteObject(bufferBitmap);
    DeleteDC(bufferDc);
}

VOID PhpHexEditUpdateScrollbars(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context
    )
{
    SCROLLINFO si = { sizeof(si) };

    si.fMask = SIF_ALL;
    si.nMin = 0;
    si.nMax = (Context->Length / Context->BytesPerRow) - 1;
    si.nPage = Context->LinesPerPage;
    si.nPos = Context->TopIndex / Context->BytesPerRow;
    SetScrollInfo(hwnd, SB_VERT, &si, TRUE);

    if (si.nMax > (LONG)si.nPage)
        EnableScrollBar(hwnd, SB_VERT, ESB_ENABLE_BOTH);

    // No horizontal scrollbar please.
    /*si.nMin = 0;
    si.nMax = ((Context->ShowAddress ? (Context->AddressIsWide ? 8 : 4) : 0) +
        (Context->ShowHex ? Context->BytesPerRow * 3 : 0) +
        (Context->ShowAscii ? Context->BytesPerRow : 0)) * Context->NullWidth;
    si.nPage = 1;
    si.nPos = 0;
    SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);*/
}

VOID PhpHexEditCreateAddressCaret(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context
    )
{
    DestroyCaret();
    CreateCaret(hwnd, NULL, Context->NullWidth * (Context->AddressIsWide ? 8 : 4), Context->LineHeight);
}

VOID PhpHexEditCreateEditCaret(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context
    )
{
    DestroyCaret();
    CreateCaret(hwnd, NULL, Context->NullWidth, Context->LineHeight);
}

VOID PhpHexEditRepositionCaret(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context,
    _In_ LONG Position
    )
{
    ULONG x;
    ULONG y;
    RECT rect;

    x = (Position - Context->TopIndex) % Context->BytesPerRow;
    y = (Position - Context->TopIndex) / Context->BytesPerRow;

    switch (Context->CurrentMode)
    {
    case EDIT_NONE:
        PhpHexEditCreateAddressCaret(hwnd, Context);
        x = 0;
        break;
    case EDIT_HIGH:
        PhpHexEditCreateEditCaret(hwnd, Context);
        x *= Context->NullWidth * 3;
        x += Context->HexOffset;
        break;
    case EDIT_LOW:
        PhpHexEditCreateEditCaret(hwnd, Context);
        x *= Context->NullWidth * 3;
        x += Context->NullWidth;
        x += Context->HexOffset;
        break;
    case EDIT_ASCII:
        PhpHexEditCreateEditCaret(hwnd, Context);
        x *= Context->NullWidth;
        x += Context->AsciiOffset;
        break;
    }

    Context->EditPosition.x = x;
    Context->EditPosition.y = y * Context->LineHeight;

    GetClientRect(hwnd, &rect);

    if (PtInRect(&rect, Context->EditPosition))
    {
        SetCaretPos(Context->EditPosition.x, Context->EditPosition.y);
        ShowCaret(hwnd);
    }
}

VOID PhpHexEditCalculatePosition(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context,
    _In_ LONG X,
    _In_ LONG Y,
    _Out_ POINT *Point
    )
{
    LONG xp;

    Y /= Context->LineHeight;

    if (Y < 0 || Y >= Context->LinesPerPage)
    {
        Point->x = -1;
        Point->y = -1;
        return;
    }

    if (Y * Context->BytesPerRow >= Context->Length)
    {
        Point->x = -1;
        Point->y = -1;
        return;
    }

    X += Context->NullWidth;
    X /= Context->NullWidth;

    if (Context->ShowAddress && X <= (Context->AddressIsWide ? 8 : 4))
    {
        Context->CurrentAddress = Context->TopIndex + Context->BytesPerRow * Y;
        Context->CurrentMode = EDIT_NONE;

        Point->x = 0;
        Point->y = Y;
        return;
    }

    xp = Context->HexOffset / Context->NullWidth + Context->BytesPerRow * 3;

    if (Context->ShowHex && X < xp)
    {
        if (X % 3)
            X--;

        Context->CurrentAddress = Context->TopIndex +
            Context->BytesPerRow * Y +
            (X - (Context->HexOffset / Context->NullWidth)) / 3;
        Context->CurrentMode = ((X % 3) & 1) ? EDIT_LOW : EDIT_HIGH;

        Point->x = X;
        Point->y = Y;
        return;
    }

    X--; // fix selection problem

    xp = Context->AsciiOffset / Context->NullWidth + Context->BytesPerRow;

    if (Context->ShowAscii && X * Context->NullWidth >= Context->AsciiOffset && X <= xp)
    {
        Context->CurrentAddress = Context->TopIndex +
            Context->BytesPerRow * Y +
            (X - (Context->AsciiOffset / Context->NullWidth));
        Context->CurrentMode = EDIT_ASCII;

        Point->x = X;
        Point->y = Y;
        return;
    }

    Point->x = -1;
    Point->y = -1;
}

VOID PhpHexEditMove(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context,
    _In_ LONG X,
    _In_ LONG Y
    )
{
    switch (Context->CurrentMode)
    {
    case EDIT_NONE:
        Context->CurrentAddress += Y * Context->BytesPerRow;
        break;
    case EDIT_HIGH:
        if (X != 0)
            Context->CurrentMode = EDIT_LOW;
        if (X == -1)
            Context->CurrentAddress--;
        Context->CurrentAddress += Y * Context->BytesPerRow;
        break;
    case EDIT_LOW:
        if (X != 0)
            Context->CurrentMode = EDIT_HIGH;
        if (X == 1)
            Context->CurrentAddress++;
        Context->CurrentAddress += Y * Context->BytesPerRow;
        break;
    case EDIT_ASCII:
        Context->CurrentAddress += X;
        Context->CurrentAddress += Y * Context->BytesPerRow;
        break;
    }

    if (Context->CurrentAddress < 0)
        Context->CurrentAddress = 0;

    if (Context->CurrentAddress >= Context->Length)
    {
        Context->CurrentAddress -= X;
        Context->CurrentAddress -= Y * Context->BytesPerRow;
    }

    Context->NoAddressChange = TRUE;

    if (Context->CurrentAddress < Context->TopIndex)
        SendMessage(hwnd, WM_VSCROLL, SB_LINEUP, 0);
    if (Context->CurrentAddress >= Context->TopIndex + Context->LinesPerPage * Context->BytesPerRow)
        SendMessage(hwnd, WM_VSCROLL, SB_LINEDOWN, 0);

    Context->NoAddressChange = FALSE;
    PhpHexEditRepositionCaret(hwnd, Context, Context->CurrentAddress);
}

VOID PhpHexEditSetSel(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context,
    _In_ LONG S,
    _In_ LONG E
    )
{
    DestroyCaret();
    Context->SelStart = S;
    Context->SelEnd = E;
    REDRAW_WINDOW(hwnd);

    if (S != -1 && E != -1)
    {
        Context->CurrentAddress = S;
    }
    else
    {
        if (Context->EditPosition.x == 0 && Context->ShowAddress)
            PhpHexEditCreateAddressCaret(hwnd, Context);
        else
            PhpHexEditCreateEditCaret(hwnd, Context);

        SetCaretPos(Context->EditPosition.x, Context->EditPosition.y);
        ShowCaret(hwnd);
    }
}

VOID PhpHexEditScrollTo(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context,
    _In_ LONG Position
    )
{
    if (Position < Context->TopIndex || Position > Context->TopIndex + Context->LinesPerPage * Context->BytesPerRow)
    {
        Context->TopIndex = Position / Context->BytesPerRow * Context->BytesPerRow; // round down
        Context->TopIndex -= Context->LinesPerPage / 3 * Context->BytesPerRow;

        if (Context->TopIndex < 0)
            Context->TopIndex = 0;

        if (Context->Length >= Context->LinesPerPage * Context->BytesPerRow)
        {
            if (Context->TopIndex > Context->Length - Context->LinesPerPage * Context->BytesPerRow)
                Context->TopIndex = Context->Length - Context->LinesPerPage * Context->BytesPerRow;
        }

        PhpHexEditUpdateScrollbars(hwnd, Context);
        REDRAW_WINDOW(hwnd);
    }
}

VOID PhpHexEditClearEdit(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context
    )
{
    if (Context->AllowLengthChange)
    {
        Context->CurrentAddress = Context->SelStart;
        PhpHexEditSelDelete(hwnd, Context, Context->SelStart, Context->SelEnd);
        PhpHexEditRepositionCaret(hwnd, Context, Context->CurrentAddress);
        REDRAW_WINDOW(hwnd);
    }
}

VOID PhpHexEditCopyEdit(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context
    )
{
    if (OpenClipboard(hwnd))
    {
        EmptyClipboard();
        PhpHexEditNormalizeSel(hwnd, Context);

        if (Context->CurrentMode != EDIT_ASCII)
        {
            ULONG length = Context->SelEnd - Context->SelStart;
            HGLOBAL binaryMemory;
            HGLOBAL hexMemory;

            binaryMemory = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, length);

            if (binaryMemory)
            {
                PUCHAR p = GlobalLock(binaryMemory);
                memcpy(p, &Context->Data[Context->SelStart], length);
                GlobalUnlock(binaryMemory);

                hexMemory = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, (length * 3 + 1) * sizeof(WCHAR));

                if (hexMemory)
                {
                    PWCHAR pw;
                    ULONG i;

                    pw = GlobalLock(hexMemory);

                    for (i = 0; i < length; i++)
                    {
                        TO_HEX(pw, Context->Data[Context->SelStart + i]);
                        *pw++ = ' ';
                    }
                    *pw = 0;

                    GlobalUnlock(hexMemory);

                    SetClipboardData(CF_UNICODETEXT, hexMemory);
                }

                SetClipboardData(RegisterClipboardFormat(L"BinaryData"), binaryMemory);
            }
        }
        else
        {
            ULONG length = Context->SelEnd - Context->SelStart;
            HGLOBAL binaryMemory;
            HGLOBAL asciiMemory;

            binaryMemory = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, length);
            asciiMemory = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, length + 1);

            if (binaryMemory)
            {
                PUCHAR p = GlobalLock(binaryMemory);
                memcpy(p, &Context->Data[Context->SelStart], length);
                GlobalUnlock(binaryMemory);

                if (asciiMemory)
                {
                    ULONG i;

                    p = GlobalLock(asciiMemory);
                    memcpy(p, &Context->Data[Context->SelStart], length);

                    for (i = 0; i < length; i++)
                    {
                        if (!IS_PRINTABLE(*p))
                            *p = '.';
                        p++;
                    }
                    *p = 0;

                    GlobalUnlock(asciiMemory);

                    SetClipboardData(CF_TEXT, asciiMemory);
                }

                SetClipboardData(RegisterClipboardFormat(L"BinaryData"), binaryMemory);
            }
        }

        CloseClipboard();
    }
}

VOID PhpHexEditCutEdit(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context
    )
{
    if (Context->AllowLengthChange)
    {
        PhpHexEditCopyEdit(hwnd, Context);
        PhpHexEditSelDelete(hwnd, Context, Context->SelStart, Context->SelEnd);
        REDRAW_WINDOW(hwnd);
    }
}

VOID PhpHexEditPasteEdit(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context
    )
{
    if (OpenClipboard(hwnd))
    {
        HANDLE memory;

        memory = GetClipboardData(RegisterClipboardFormat(L"BinaryData"));

        if (!memory)
            memory = GetClipboardData(CF_TEXT);

        if (memory)
        {
            PUCHAR p = GlobalLock(memory);
            ULONG length = (ULONG)GlobalSize(memory);
            ULONG paste;
            ULONG oldCurrentAddress = Context->CurrentAddress;

            PhpHexEditNormalizeSel(hwnd, Context);

            if (Context->AllowLengthChange)
            {
                if (Context->SelStart == -1)
                {
                    if (Context->CurrentMode == EDIT_LOW)
                        Context->CurrentAddress++;

                    paste = Context->CurrentAddress;
                    PhpHexEditSelInsert(hwnd, Context, Context->CurrentAddress, length);
                }
                else
                {
                    paste = Context->SelStart;
                    PhpHexEditSelDelete(hwnd, Context, Context->SelStart, Context->SelEnd);
                    PhpHexEditSelInsert(hwnd, Context, paste, length);
                    PhpHexEditSetSel(hwnd, Context, -1, -1);
                }
            }
            else
            {
                if (Context->SelStart == -1)
                {
                    if (Context->CurrentMode == EDIT_LOW)
                        Context->CurrentAddress++;

                    paste = Context->CurrentAddress;
                }
                else
                {
                    paste = Context->SelStart;
                }

                if (length > Context->Length - paste)
                    length = Context->Length - paste;
            }

            memcpy(&Context->Data[paste], p, length);
            GlobalUnlock(memory);

            Context->CurrentAddress = oldCurrentAddress;
            REDRAW_WINDOW(hwnd);
        }

        CloseClipboard();
    }
}

VOID PhpHexEditSelectAll(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context
    )
{
    Context->SelStart = 0;
    Context->SelEnd = Context->Length;
    DestroyCaret();
    REDRAW_WINDOW(hwnd);
}

VOID PhpHexEditUndoEdit(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context
    )
{
    // TODO
}

VOID PhpHexEditNormalizeSel(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context
    )
{
    if (Context->SelStart > Context->SelEnd)
    {
        LONG t;

        t = Context->SelEnd;
        Context->SelEnd = Context->SelStart;
        Context->SelStart = t;
    }
}

VOID PhpHexEditSelDelete(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context,
    _In_ LONG S,
    _In_ LONG E
    )
{
    if (Context->AllowLengthChange && Context->Length > 0)
    {
        PUCHAR p = PhAllocate(Context->Length - (E - S) + 1);

        memcpy(p, Context->Data, S);

        if (S < Context->Length - (E - S))
            memcpy(&p[S], &Context->Data[E], Context->Length - E);

        PhFree(Context->Data);
        Context->Data = p;
        PhpHexEditSetSel(hwnd, Context, -1, -1);
        Context->Length -= E - S;

        if (Context->CurrentAddress > Context->Length)
        {
            Context->CurrentAddress = Context->Length;
            PhpHexEditRepositionCaret(hwnd, Context, Context->CurrentAddress);
        }

        Context->Update = TRUE;
    }
}

VOID PhpHexEditSelInsert(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context,
    _In_ LONG S,
    _In_ LONG L
    )
{
    if (Context->AllowLengthChange)
    {
        PUCHAR p = PhAllocate(Context->Length + L);

        memset(p, 0, Context->Length + L);
        memcpy(p, Context->Data, S);
        memcpy(&p[S + L], &Context->Data[S], Context->Length - S);

        PhFree(Context->Data);
        Context->Data = p;
        PhpHexEditSetSel(hwnd, Context, -1, -1);
        Context->Length += L;

        Context->Update = TRUE;
    }
}

VOID PhpHexEditSetBuffer(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context,
    _In_ PUCHAR Data,
    _In_ ULONG Length
    )
{
    Context->Data = Data;
    PhpHexEditSetSel(hwnd, Context, -1, -1);
    Context->Length = Length;
    Context->CurrentAddress = 0;
    Context->EditPosition.x = Context->EditPosition.y = 0;
    Context->CurrentMode = EDIT_HIGH;
    Context->TopIndex = 0;
    Context->Update = TRUE;

    Context->UserBuffer = TRUE;
    Context->AllowLengthChange = FALSE;
}

VOID PhpHexEditSetData(
    _In_ HWND hwnd,
    _In_ PPHP_HEXEDIT_CONTEXT Context,
    _In_ PUCHAR Data,
    _In_ ULONG Length
    )
{
    if (Context->Data) PhFree(Context->Data);
    Context->Data = PhAllocate(Length);
    memcpy(Context->Data, Data, Length);
    PhpHexEditSetBuffer(hwnd, Context, Context->Data, Length);
    Context->UserBuffer = FALSE;
    Context->AllowLengthChange = TRUE;
}
