/*
 * PROJECT:    PAINT for ReactOS
 * LICENSE:    LGPL-2.0-or-later (https://spdx.org/licenses/LGPL-2.0-or-later)
 * PURPOSE:    Providing the canvas window class
 * COPYRIGHT:  Copyright 2015 Benedikt Freisen <b.freisen@gmx.net>
 *             Copyright 2026 Katayama Hirofumi MZ <katayama.hirofumi.mz@gmail.com>
 */

#include "precomp.h"

CCanvasWindow canvasWindow;

/* FUNCTIONS ********************************************************/

HCURSOR
CStyledCursor::CreateStyledCursor(BrushStyle style, INT zoom, INT radius, COLORREF color, BOOL is_rubber)
{
    const INT diameter = 2 * radius;
    if (diameter * zoom / DEFAULT_ZOOM <= 2)
    {
        HCURSOR hCursor = ::LoadCursor(NULL, IDC_CROSS);
        return hCursor ? CopyCursor(hCursor) : NULL;
    }

    const INT crosshair1 = 6, crosshair2 = crosshair1 - 2;
    const INT width = diameter + 2 * crosshair1, height = diameter + 2 * crosshair1;
    DWORD hotX = width / 2, hotY = height / 2;

    HDC hdcScreen = ::GetDC(NULL);
    if (!hdcScreen)
        return NULL;
    HDC hdcMem = ::CreateCompatibleDC(hdcScreen);
    if (!hdcMem)
    {
        ::ReleaseDC(NULL, hdcScreen);
        return NULL;
    }

    RECT rc = { 0, 0, width, height };

    // Create the AND mask bitmap. This must be monochrome (1bpp):
    // white bits are transparent, black bits are opaque.
    HBITMAP hbmMask = ::CreateBitmap(width, height, 1, 1, NULL);
    if (hbmMask)
    {
        HBITMAP hbmOld = (HBITMAP)::SelectObject(hdcMem, hbmMask);

        // Fill with white brush
        ::FillRect(hdcMem, &rc, (HBRUSH)::GetStockObject(WHITE_BRUSH));

        if (!is_rubber)
        {
            // Draw crosshair with white pen
            ::SelectObject(hdcMem, (HPEN)::GetStockObject(WHITE_PEN));
            ::MoveToEx(hdcMem, 0, hotY, NULL);
            ::LineTo(hdcMem, crosshair2, hotY);
            ::MoveToEx(hdcMem, width - crosshair2, hotY, NULL);
            ::LineTo(hdcMem, width, hotY);
            ::MoveToEx(hdcMem, hotX, 0, NULL);
            ::LineTo(hdcMem, hotX, crosshair2);
            ::MoveToEx(hdcMem, hotX, height - crosshair2, NULL);
            ::LineTo(hdcMem, hotX, height);
        }

        // Draw brush or erase with black color
        if (is_rubber)
            Erase(hdcMem, hotX, hotY, hotX, hotY, RGB(0, 0, 0), radius + 1);
        else
            Brush(hdcMem, hotX, hotY, hotX, hotY, RGB(0, 0, 0), style, diameter);

        if (is_rubber)
            InflateRect(&rc, -1, -1);

        ::SelectObject(hdcMem, hbmOld);
    }

    // Create the color (XOR) bitmap
    HBITMAP hbmColor = ::CreateCompatibleBitmap(hdcScreen, width, height);
    if (hbmColor)
    {
        HBITMAP hbmOld = (HBITMAP)::SelectObject(hdcMem, hbmColor);

        // Fill with black brush
        ::FillRect(hdcMem, &rc, (HBRUSH)::GetStockObject(BLACK_BRUSH));

        if (is_rubber)
        {
            // Draw for rubber border
            INT avg = (GetRValue(color) + GetGValue(color) + GetBValue(color)) / 3;
            COLORREF color2 = (avg > 255 / 2) ? RGB(0, 0, 0) : RGB(255, 255, 255);
            Erase(hdcMem, hotX, hotY, hotX, hotY, color2, radius + 1);
        }
        else
        {
            // Draw crosshair with white pen
            ::SelectObject(hdcMem, (HPEN)::GetStockObject(WHITE_PEN));
            ::MoveToEx(hdcMem, 0, hotY, NULL);
            ::LineTo(hdcMem, crosshair2, hotY);
            ::MoveToEx(hdcMem, width - crosshair2, hotY, NULL);
            ::LineTo(hdcMem, width, hotY);
            ::MoveToEx(hdcMem, hotX, 0, NULL);
            ::LineTo(hdcMem, hotX, crosshair2);
            ::MoveToEx(hdcMem, hotX, height - crosshair2, NULL);
            ::LineTo(hdcMem, hotX, height);
        }

        // Draw brush or erase with color
        if (is_rubber)
            Erase(hdcMem, hotX, hotY, hotX, hotY, color, radius);
        else
            Brush(hdcMem, hotX, hotY, hotX, hotY, color, style, diameter);

        ::SelectObject(hdcMem, hbmOld);
    }

    ::ReleaseDC(NULL, hdcScreen);
    ::DeleteDC(hdcMem);

    if (zoom != DEFAULT_ZOOM)
    {
        INT newWidth = width * zoom / DEFAULT_ZOOM;
        INT newHeight = height * zoom / DEFAULT_ZOOM;
        HBITMAP hbmMaskNew = CopyMonoImage(hbmMask, newWidth, newHeight, STRETCH_DELETESCANS);
        HBITMAP hbmColorNew = CopyDIBImage(hbmColor, newWidth, newHeight, STRETCH_DELETESCANS);
        ::DeleteObject(hbmMask);
        ::DeleteObject(hbmColor);
        hbmMask = hbmMaskNew;
        hbmColor = hbmColorNew;
        hotX = width * zoom / (2 * DEFAULT_ZOOM);
        hotY = height * zoom / (2 * DEFAULT_ZOOM);
    }

    ICONINFO ii = { FALSE, hotX, hotY, hbmMask, hbmColor };
    HCURSOR hCursor = (HCURSOR)::CreateIconIndirect(&ii);

    ::DeleteObject(hbmMask);
    ::DeleteObject(hbmColor);

    return hCursor;
}

void CStyledCursor::SetStyle(BrushStyle style, INT zoom, INT radius, COLORREF color, BOOL is_rubber)
{
    if (m_hCursor &&
        m_style == style &&
        m_zoom == zoom &&
        m_radius == radius &&
        m_color == color &&
        m_is_rubber == is_rubber)
    {
        return;
    }

    if (m_hCursor)
        DestroyCursor(m_hCursor);

    m_hCursor = CreateStyledCursor(style, zoom, radius, color, is_rubber);
    m_style = style;
    m_zoom = zoom;
    m_radius = radius;
    m_color = color;
    m_is_rubber = is_rubber;
}

CCanvasWindow::CCanvasWindow()
    : m_drawing(FALSE)
    , m_hitCanvasSizeBox(HIT_NONE)
    , m_ptOrig { -1, -1 }
{
    m_rcResizing.SetRectEmpty();
}

CCanvasWindow::~CCanvasWindow()
{
}

RECT CCanvasWindow::GetBaseRect()
{
    CRect rcBase;
    GetImageRect(rcBase);
    ImageToCanvas(rcBase);
    rcBase.InflateRect(GRIP_SIZE, GRIP_SIZE);
    return rcBase;
}

VOID CCanvasWindow::ImageToCanvas(POINT& pt)
{
    Zoomed(pt);
    pt.x += GRIP_SIZE - GetScrollPos(SB_HORZ);
    pt.y += GRIP_SIZE - GetScrollPos(SB_VERT);
}

VOID CCanvasWindow::ImageToCanvas(RECT& rc)
{
    Zoomed(rc);
    ::OffsetRect(&rc, GRIP_SIZE - GetScrollPos(SB_HORZ), GRIP_SIZE - GetScrollPos(SB_VERT));
}

VOID CCanvasWindow::CanvasToImage(POINT& pt, BOOL bRound)
{
    pt.x -= GRIP_SIZE - GetScrollPos(SB_HORZ);
    pt.y -= GRIP_SIZE - GetScrollPos(SB_VERT);
    UnZoomed(pt, bRound);
}

VOID CCanvasWindow::CanvasToImage(RECT& rc, BOOL bRound)
{
    ::OffsetRect(&rc, GetScrollPos(SB_HORZ) - GRIP_SIZE, GetScrollPos(SB_VERT) - GRIP_SIZE);
    UnZoomed(rc, bRound);
}

VOID CCanvasWindow::GetImageRect(RECT& rc)
{
    rc = { 0, 0, imageModel.GetWidth(), imageModel.GetHeight() };
}

HITTEST CCanvasWindow::CanvasHitTest(POINT pt)
{
    if (selectionModel.m_bShow || ::IsWindowVisible(textEditWindow))
        return HIT_INNER;
    RECT rcBase = GetBaseRect();
    return getSizeBoxHitTest(pt, &rcBase);
}

VOID CCanvasWindow::getNewZoomRect(CRect& rcView, INT newZoom, CPoint ptTarget)
{
    CRect rcImage;
    GetImageRect(rcImage);
    ImageToCanvas(rcImage);

    // Calculate the zoom rectangle
    INT oldZoom = toolsModel.GetZoom();
    GetClientRect(rcView);
    LONG cxView = rcView.right * oldZoom / newZoom, cyView = rcView.bottom * oldZoom / newZoom;
    rcView.SetRect(ptTarget.x - cxView / 2, ptTarget.y - cyView / 2,
                   ptTarget.x + cxView / 2, ptTarget.y + cyView / 2);

    // Shift the rectangle if necessary
    INT dx = 0, dy = 0;
    if (rcView.left < rcImage.left)
        dx = rcImage.left - rcView.left;
    else if (rcImage.right < rcView.right)
        dx = rcImage.right - rcView.right;
    if (rcView.top < rcImage.top)
        dy = rcImage.top - rcView.top;
    else if (rcImage.bottom < rcView.bottom)
        dy = rcImage.bottom - rcView.bottom;
    rcView.OffsetRect(dx, dy);

    rcView.IntersectRect(&rcView, &rcImage);
}

VOID CCanvasWindow::zoomTo(INT newZoom, LONG left, LONG top)
{
    POINT pt = { left, top };
    CanvasToImage(pt);

    toolsModel.SetZoom(newZoom);
    ImageToCanvas(pt);
    pt.x += GetScrollPos(SB_HORZ);
    pt.y += GetScrollPos(SB_VERT);

    updateScrollRange();
    updateScrollPos(pt.x, pt.y);
    Invalidate(TRUE);
}

BOOL CCanvasWindow::DoDraw(HDC hDC, RECT& rcClient, RECT& rcPaint)
{
    // This is the target area we have to draw on
    CRect rcCanvasDraw;
    rcCanvasDraw.IntersectRect(&rcClient, &rcPaint);

    // Calculate image size
    CRect rcImage;
    GetImageRect(rcImage);
    SIZE sizeImage = { imageModel.GetWidth(), imageModel.GetHeight() };

    // We use a memory bitmap to reduce flickering
    HBITMAP hbmCache1 = CreateDIBWithProperties(rcClient.right, rcClient.bottom);
    if (!hbmCache1)
        return FALSE; // Out of memory
    HBITMAP hbmCache2 = CreateDIBWithProperties(sizeImage.cx, sizeImage.cy);
    if (!hbmCache2)
    {
        ::DeleteObject(hbmCache1);
        return FALSE; // Out of memory
    }

    HDC hdcMem0 = ::CreateCompatibleDC(hDC);
    HGDIOBJ hbm0Old = ::SelectObject(hdcMem0, hbmCache1);

    // Fill the background on hdcMem0
    ::FillRect(hdcMem0, &rcCanvasDraw, (HBRUSH)(COLOR_APPWORKSPACE + 1));

    // Draw the sizeboxes if necessary
    RECT rcBase = GetBaseRect();
    if (!selectionModel.m_bShow && !::IsWindowVisible(textEditWindow))
        drawSizeBoxes(hdcMem0, &rcBase, FALSE, &rcCanvasDraw);

    // Calculate the target area on the image
    CRect rcImageDraw = rcCanvasDraw;
    CanvasToImage(rcImageDraw);
    rcImageDraw.IntersectRect(&rcImageDraw, &rcImage);

    // Consider rounding down by zooming
    rcImageDraw.right += 1;
    rcImageDraw.bottom += 1;

    // hdcMem1 <-- imageModel
    HDC hdcMem1 = ::CreateCompatibleDC(hDC);
    HGDIOBJ hbm1Old = ::SelectObject(hdcMem1, hbmCache2);
    ::BitBlt(hdcMem1, rcImageDraw.left, rcImageDraw.top, rcImageDraw.Width(), rcImageDraw.Height(),
             imageModel.GetDC(), rcImageDraw.left, rcImageDraw.top, SRCCOPY);

    // Draw overlay #1 on hdcMem1
    toolsModel.OnDrawOverlayOnImage(hdcMem1);

    // Transfer the bits with stretch (hdcMem0 <-- hdcMem1)
    ImageToCanvas(rcImage);
    ::StretchBlt(hdcMem0, rcImage.left, rcImage.top, rcImage.Width(), rcImage.Height(),
                 hdcMem1, 0, 0, sizeImage.cx, sizeImage.cy, SRCCOPY);

    // Clean up hdcMem1
    ::SelectObject(hdcMem1, hbm1Old);
    ::DeleteDC(hdcMem1);

    // Draw the grid on hdcMem0
    if (g_showGrid && toolsModel.GetZoom() >= 4000)
    {
        HPEN oldPen = (HPEN) ::SelectObject(hdcMem0, ::CreatePen(PS_SOLID, 1, RGB(160, 160, 160)));
        for (INT counter = 0; counter < sizeImage.cy; counter++)
        {
            POINT pt0 = { 0, counter }, pt1 = { sizeImage.cx, counter };
            ImageToCanvas(pt0);
            ImageToCanvas(pt1);
            ::MoveToEx(hdcMem0, pt0.x, pt0.y, NULL);
            ::LineTo(hdcMem0, pt1.x, pt1.y);
        }
        for (INT counter = 0; counter < sizeImage.cx; counter++)
        {
            POINT pt0 = { counter, 0 }, pt1 = { counter, sizeImage.cy };
            ImageToCanvas(pt0);
            ImageToCanvas(pt1);
            ::MoveToEx(hdcMem0, pt0.x, pt0.y, NULL);
            ::LineTo(hdcMem0, pt1.x, pt1.y);
        }
        ::DeleteObject(::SelectObject(hdcMem0, oldPen));
    }

    // Draw overlay #2 on hdcMem0
    toolsModel.OnDrawOverlayOnCanvas(hdcMem0);

    // Draw new frame on hdcMem0 if any
    if (m_hitCanvasSizeBox != HIT_NONE && !m_rcResizing.IsRectEmpty())
        DrawXorRect(hdcMem0, &m_rcResizing);

    // Transfer the bits (hDC <-- hdcMem0)
    ::BitBlt(hDC, rcCanvasDraw.left, rcCanvasDraw.top, rcCanvasDraw.Width(), rcCanvasDraw.Height(),
             hdcMem0, rcCanvasDraw.left, rcCanvasDraw.top, SRCCOPY);

    // Clean up hdcMem0
    ::SelectObject(hdcMem0, hbm0Old);
    ::DeleteDC(hdcMem0);
    ::DeleteObject(hbmCache2);
    ::DeleteObject(hbmCache1);

    return TRUE;
}

VOID CCanvasWindow::updateScrollRange()
{
    CRect rcClient;
    GetClientRect(&rcClient);

    CSize sizePage(rcClient.right, rcClient.bottom);
    CSize sizeZoomed = { Zoomed(imageModel.GetWidth()), Zoomed(imageModel.GetHeight()) };
    CSize sizeWhole = { sizeZoomed.cx + (GRIP_SIZE * 2), sizeZoomed.cy + (GRIP_SIZE * 2) };

    // show/hide the scrollbars
    ShowScrollBar(SB_HORZ, sizePage.cx < sizeWhole.cx);
    ShowScrollBar(SB_VERT, sizePage.cy < sizeWhole.cy);

    if (sizePage.cx < sizeWhole.cx || sizePage.cy < sizeWhole.cy)
    {
        GetClientRect(&rcClient); // Scrollbars might change, get client rectangle again
        sizePage = CSize(rcClient.right, rcClient.bottom);
    }

    SCROLLINFO si = { sizeof(si), SIF_PAGE | SIF_RANGE };
    si.nMin   = 0;

    si.nMax   = sizeWhole.cx;
    si.nPage  = sizePage.cx;
    SetScrollInfo(SB_HORZ, &si);

    si.nMax   = sizeWhole.cy;
    si.nPage  = sizePage.cy;
    SetScrollInfo(SB_VERT, &si);
}

VOID CCanvasWindow::updateScrollPos(INT x, INT y)
{
    SetScrollPos(SB_HORZ, x);
    SetScrollPos(SB_VERT, y);
}

LRESULT CCanvasWindow::OnSize(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    if (m_hWnd)
        updateScrollRange();

    return 0;
}

VOID CCanvasWindow::OnHVScroll(WPARAM wParam, INT fnBar)
{
    SCROLLINFO si;
    si.cbSize = sizeof(si);
    si.fMask = SIF_ALL;
    GetScrollInfo(fnBar, &si);

    switch (LOWORD(wParam))
    {
        case SB_THUMBTRACK:
        case SB_THUMBPOSITION:
            si.nPos = (SHORT)HIWORD(wParam);
            break;
        case SB_LINELEFT: // SB_LINEUP
            si.nPos -= 15;
            break;
        case SB_LINERIGHT: // SB_LINEDOWN
            si.nPos += 15;
            break;
        case SB_PAGELEFT: // SB_PAGEUP
            si.nPos -= si.nPage;
            break;
        case SB_PAGERIGHT: // SB_PAGEDOWN
            si.nPos += si.nPage;
            break;
    }
    si.nPos = max(min(si.nPos, si.nMax), si.nMin);
    SetScrollInfo(fnBar, &si);
    Invalidate();
}

LRESULT CCanvasWindow::OnHScroll(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    OnHVScroll(wParam, SB_HORZ);
    return 0;
}

LRESULT CCanvasWindow::OnVScroll(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    OnHVScroll(wParam, SB_VERT);
    return 0;
}

LRESULT CCanvasWindow::OnButtonDown(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };

    m_nMouseDownMsg = nMsg;
    BOOL bLeftButton = (nMsg == WM_LBUTTONDOWN);

    if (nMsg == WM_MBUTTONDOWN)
    {
        m_ptOrig = pt;
        SetCapture();
        ::SetCursor(::LoadCursorW(g_hinstExe, MAKEINTRESOURCEW(IDC_HANDDRAG)));
        return 0;
    }

    HITTEST hitSelection = selectionModel.hitTest(pt);
    if (hitSelection != HIT_NONE)
    {
        m_drawing = TRUE;
        CanvasToImage(pt, TRUE);
        SetCapture();
        toolsModel.OnButtonDown(bLeftButton, pt.x, pt.y, FALSE);
        Invalidate();
        return 0;
    }

    HITTEST hit = CanvasHitTest(pt);
    if (hit == HIT_NONE || hit == HIT_BORDER)
    {
        switch (toolsModel.GetActiveTool())
        {
            case TOOL_BEZIER:
            case TOOL_SHAPE:
                toolsModel.OnEndDraw(TRUE);
                Invalidate();
                break;

            case TOOL_FREESEL:
            case TOOL_RECTSEL:
                toolsModel.OnEndDraw(FALSE);
                Invalidate();
                break;

            default:
                break;
        }

        toolsModel.resetTool(); // resets the point-buffer of the polygon and bezier functions
        return 0;
    }

    CanvasToImage(pt, TRUE);

    if (hit == HIT_INNER)
    {
        m_drawing = TRUE;
        SetCapture();
        toolsModel.OnButtonDown(bLeftButton, pt.x, pt.y, FALSE);
        Invalidate();
        return 0;
    }

    if (bLeftButton)
    {
        m_hitCanvasSizeBox = hit;
        m_ptOrig = pt;
        SetCapture();
    }

    return 0;
}

LRESULT CCanvasWindow::OnButtonDblClk(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
    CanvasToImage(pt, TRUE);

    m_drawing = FALSE;
    ::ReleaseCapture();
    m_nMouseDownMsg = 0;

    toolsModel.OnButtonDown(nMsg == WM_LBUTTONDBLCLK, pt.x, pt.y, TRUE);
    toolsModel.resetTool();
    Invalidate();
    return 0;
}

LRESULT CCanvasWindow::OnMouseMove(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };

    if (m_nMouseDownMsg == WM_MBUTTONDOWN)
    {
        INT x = GetScrollPos(SB_HORZ) - (pt.x - m_ptOrig.x);
        INT y = GetScrollPos(SB_VERT) - (pt.y - m_ptOrig.y);
        SendMessage(WM_HSCROLL, MAKEWPARAM(SB_THUMBPOSITION, x), 0);
        SendMessage(WM_VSCROLL, MAKEWPARAM(SB_THUMBPOSITION, y), 0);
        m_ptOrig = pt;
        return 0;
    }

    CanvasToImage(pt, TRUE);

    if (toolsModel.GetActiveTool() == TOOL_ZOOM)
        Invalidate();

    if (!m_drawing || toolsModel.GetActiveTool() <= TOOL_AIRBRUSH)
    {
        TRACKMOUSEEVENT tme = { sizeof(tme) };
        tme.dwFlags = TME_LEAVE;
        tme.hwndTrack = m_hWnd;
        tme.dwHoverTime = 0;
        ::TrackMouseEvent(&tme);

        if (!m_drawing)
        {
            CRect rcImage;
            GetImageRect(rcImage);

            CStringW strCoord;
            if (rcImage.PtInRect(pt))
                strCoord.Format(L"%ld, %ld", pt.x, pt.y);
            ::SendMessageW(g_hStatusBar, SB_SETTEXT, 1, (LPARAM)(LPCWSTR)strCoord);
        }
    }

    if (m_drawing || toolsModel.IsSelection())
    {
        toolsModel.DrawWithMouseTool(pt, wParam);
        return 0;
    }

    if (m_hitCanvasSizeBox == HIT_NONE || ::GetCapture() != m_hWnd)
        return 0;

    // Dragging now... Calculate the new size
    INT cxImage = imageModel.GetWidth(), cyImage = imageModel.GetHeight();
    INT cxDelta = pt.x - m_ptOrig.x;
    INT cyDelta = pt.y - m_ptOrig.y;
    switch (m_hitCanvasSizeBox)
    {
        case HIT_UPPER_LEFT:
            cxImage -= cxDelta;
            cyImage -= cyDelta;
            break;
        case HIT_UPPER_CENTER:
            cyImage -= cyDelta;
            break;
        case HIT_UPPER_RIGHT:
            cxImage += cxDelta;
            cyImage -= cyDelta;
            break;
        case HIT_MIDDLE_LEFT:
            cxImage -= cxDelta;
            break;
        case HIT_MIDDLE_RIGHT:
            cxImage += cxDelta;
            break;
        case HIT_LOWER_LEFT:
            cxImage -= cxDelta;
            cyImage += cyDelta;
            break;
        case HIT_LOWER_CENTER:
            cyImage += cyDelta;
            break;
        case HIT_LOWER_RIGHT:
            cxImage += cxDelta;
            cyImage += cyDelta;
            break;
        default:
            return 0;
    }

    // Limit bitmap size
    cxImage = max(1, cxImage);
    cyImage = max(1, cyImage);
    cxImage = min(MAXWORD, cxImage);
    cyImage = min(MAXWORD, cyImage);

    // Display new size
    CStringW strSize;
    strSize.Format(L"%d x %d", cxImage, cyImage);
    ::SendMessageW(g_hStatusBar, SB_SETTEXT, 2, (LPARAM)(LPCWSTR)strSize);

    // Dragging now... Fix the position...
    CRect rcResizing = { 0, 0, cxImage, cyImage };
    switch (m_hitCanvasSizeBox)
    {
        case HIT_UPPER_LEFT:
            rcResizing.OffsetRect(cxDelta, cyDelta);
            break;
        case HIT_UPPER_CENTER:
            rcResizing.OffsetRect(0, cyDelta);
            break;
        case HIT_UPPER_RIGHT:
            rcResizing.OffsetRect(0, cyDelta);
            break;
        case HIT_MIDDLE_LEFT:
            rcResizing.OffsetRect(cxDelta, 0);
            break;
        case HIT_LOWER_LEFT:
            rcResizing.OffsetRect(cxDelta, 0);
            break;
        default:
            break;
    }
    ImageToCanvas(rcResizing);
    m_rcResizing = rcResizing;
    Invalidate(TRUE);

    return 0;
}

LRESULT CCanvasWindow::OnButtonUp(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
    CanvasToImage(pt, TRUE);

    ::ReleaseCapture();

    BOOL bLeftButton = (m_nMouseDownMsg == WM_LBUTTONDOWN);
    m_nMouseDownMsg = 0;

    if (m_drawing)
    {
        m_drawing = FALSE;
        toolsModel.OnButtonUp(bLeftButton, pt.x, pt.y);
        Invalidate(FALSE);
        ::SendMessageW(g_hStatusBar, SB_SETTEXT, 2, (LPARAM)L"");
        return 0;
    }

    if (m_hitCanvasSizeBox == HIT_NONE || !bLeftButton)
        return 0;

    // Resize the image
    INT cxImage = imageModel.GetWidth(), cyImage = imageModel.GetHeight();
    INT cxDelta = pt.x - m_ptOrig.x;
    INT cyDelta = pt.y - m_ptOrig.y;
    switch (m_hitCanvasSizeBox)
    {
        case HIT_UPPER_LEFT:
            imageModel.Crop(cxImage - cxDelta, cyImage - cyDelta, cxDelta, cyDelta);
            break;
        case HIT_UPPER_CENTER:
            imageModel.Crop(cxImage, cyImage - cyDelta, 0, cyDelta);
            break;
        case HIT_UPPER_RIGHT:
            imageModel.Crop(cxImage + cxDelta, cyImage - cyDelta, 0, cyDelta);
            break;
        case HIT_MIDDLE_LEFT:
            imageModel.Crop(cxImage - cxDelta, cyImage, cxDelta, 0);
            break;
        case HIT_MIDDLE_RIGHT:
            imageModel.Crop(cxImage + cxDelta, cyImage, 0, 0);
            break;
        case HIT_LOWER_LEFT:
            imageModel.Crop(cxImage - cxDelta, cyImage + cyDelta, cxDelta, 0);
            break;
        case HIT_LOWER_CENTER:
            imageModel.Crop(cxImage, cyImage + cyDelta, 0, 0);
            break;
        case HIT_LOWER_RIGHT:
            imageModel.Crop(cxImage + cxDelta, cyImage + cyDelta, 0, 0);
            break;
        default:
            break;
    }
    m_rcResizing.SetRectEmpty();

    g_imageSaved = FALSE;

    m_hitCanvasSizeBox = HIT_NONE;
    toolsModel.resetTool(); // resets the point-buffer of the polygon and bezier functions
    updateScrollRange();
    Invalidate(TRUE);
    return 0;
}

LRESULT CCanvasWindow::OnSetCursor(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    if (CWaitCursor::IsWaiting())
    {
        bHandled = FALSE;
        return 0;
    }

    if (m_nMouseDownMsg == WM_MBUTTONDOWN)
    {
        ::SetCursor(::LoadCursorW(g_hinstExe, MAKEINTRESOURCEW(IDC_HANDDRAG)));
        return 0;
    }

    POINT pt;
    ::GetCursorPos(&pt);
    ScreenToClient(&pt);

    CRect rcClient;
    GetClientRect(&rcClient);

    if (!rcClient.PtInRect(pt))
    {
        bHandled = FALSE;
        return 0;
    }

    HITTEST hitSelection = selectionModel.hitTest(pt);
    if (hitSelection != HIT_NONE)
    {
        if (!setCursorOnSizeBox(hitSelection))
            ::SetCursor(::LoadCursorW(NULL, (LPCWSTR)IDC_SIZEALL));
        return 0;
    }

    CRect rcImage;
    GetImageRect(rcImage);
    ImageToCanvas(rcImage);

    if (rcImage.PtInRect(pt))
    {
        switch (toolsModel.GetActiveTool())
        {
            case TOOL_FILL:
                ::SetCursor(::LoadCursorW(g_hinstExe, MAKEINTRESOURCEW(IDC_FILL)));
                break;
            case TOOL_COLOR:
                ::SetCursor(::LoadCursorW(g_hinstExe, MAKEINTRESOURCEW(IDC_COLOR)));
                break;
            case TOOL_ZOOM:
                ::SetCursor(::LoadCursorW(g_hinstExe, MAKEINTRESOURCEW(IDC_ZOOM)));
                break;
            case TOOL_PEN:
                ::SetCursor(::LoadCursorW(g_hinstExe, MAKEINTRESOURCEW(IDC_PEN)));
                break;
            case TOOL_AIRBRUSH:
                ::SetCursor(::LoadCursorW(g_hinstExe, MAKEINTRESOURCEW(IDC_AIRBRUSH)));
                break;
            case TOOL_RUBBER:
            {
                m_hRubberCursor.SetStyle(BrushStyleSquare,
                                         toolsModel.GetZoom(),
                                         toolsModel.GetRubberRadius(),
                                         paletteModel.GetBgColor(), TRUE);
                m_hRubberCursor.SetCursor();
                break;
            }
            case TOOL_BRUSH:
            {
                m_hBrushCursor.SetStyle(toolsModel.GetBrushStyle(),
                                        toolsModel.GetZoom(),
                                        toolsModel.GetBrushWidth() / 2,
                                        paletteModel.GetFgColor(), FALSE);
                m_hBrushCursor.SetCursor();
                break;
            }
            default:
                ::SetCursor(::LoadCursorW(NULL, (LPCWSTR)IDC_CROSS));
        }
        return 0;
    }

    if (selectionModel.m_bShow || !setCursorOnSizeBox(CanvasHitTest(pt)))
        bHandled = FALSE;

    return 0;
}

LRESULT CCanvasWindow::OnKeyDown(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    if (wParam == VK_ESCAPE)
    {
        OnEndDraw(TRUE);
        ::ReleaseCapture();
        m_nMouseDownMsg = 0;
        m_hitCanvasSizeBox = HIT_NONE;
        m_rcResizing.SetRectEmpty();
        Invalidate(TRUE);
    }

    return 0;
}

LRESULT CCanvasWindow::OnCancelMode(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    // Cancel dragging
    m_hitCanvasSizeBox = HIT_NONE;
    m_rcResizing.SetRectEmpty();
    Invalidate(TRUE);
    return 0;
}

LRESULT CCanvasWindow::OnMouseWheel(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    return ::SendMessageW(GetParent(), nMsg, wParam, lParam);
}

LRESULT CCanvasWindow::OnCaptureChanged(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    ::SendMessageW(g_hStatusBar, SB_SETTEXT, 2, (LPARAM)L"");
    return 0;
}

LRESULT CCanvasWindow::OnEraseBkgnd(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    return TRUE; // do nothing => transparent background
}

LRESULT CCanvasWindow::OnPaint(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    RECT rcClient;
    GetClientRect(&rcClient);

    static BOOL s_bShowedOutOfMemory = FALSE; // Don't show "Out Of Memory" message multiple time

    PAINTSTRUCT ps;
    HDC hDC = BeginPaint(&ps);

    if (DoDraw(hDC, rcClient, ps.rcPaint))
    {
        s_bShowedOutOfMemory = FALSE;
    }
    else if (!s_bShowedOutOfMemory)
    {
        ShowOutOfMemory();
        s_bShowedOutOfMemory = TRUE;
        imageModel.ClearHistory(); // Reduce memory usage
    }

    EndPaint(&ps);
    return 0;
}

VOID CCanvasWindow::OnEndDraw(BOOL bCancel)
{
    m_drawing = FALSE;
    toolsModel.OnEndDraw(bCancel);
    Invalidate(FALSE);
}

LRESULT CCanvasWindow::OnCtlColorEdit(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    SetTextColor((HDC)wParam, paletteModel.GetFgColor());
    SetBkMode((HDC)wParam, TRANSPARENT);
    return (LRESULT)GetStockObject(NULL_BRUSH);
}

LRESULT CCanvasWindow::OnPaletteModelColorChanged(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    imageModel.NotifyImageChanged();
    return 0;
}
