/* Modified version of DialogSizer_Set.cpp. See DialogSizer.h for the original Copyright */

#include "BaseUtil.h"
#include "DialogSizer.h"
#include "WinUtil.h"

#define DIALOG_DATA_PROPERTY L"GipsySoftDialogSizerData"

static LRESULT CALLBACK SizingProc(HWND, UINT, WPARAM, LPARAM);

class DialogData {
  public:
    DialogData(HWND hwnd, const DialogSizerSizingItem *psd, bool bShowSizingGrip)
        : hwnd(hwnd), bMaximised(false), bShowSizingGrip(bShowSizingGrip) {
        // Given an array of dialog item structures determine how many of them there
        // are by scanning along them until we reach the last.
        nItemCount = 0;
        for (const DialogSizerSizingItem *psi = psd; psi->uSizeInfo != 0xFFFFFFFF; psi++)
            nItemCount++;

        // Copy all of the user controls etc. for later, this way the user can quite happily
        // let the structure go out of scope.
        this->psd = (DialogSizerSizingItem *)memdup((void *)psd,
                                                    nItemCount * sizeof(DialogSizerSizingItem));
        if (!this->psd)
            nItemCount = 0;

        // Store some sizes etc. for later.
        WindowRect rectWnd(hwnd);
        ptSmallest.x = rectWnd.dx;
        ptSmallest.y = rectWnd.dy;

        ClientRect rectClient(hwnd);
        sizeClient = rectClient.Size();
        UpdateGripperRect();

        // Because we have successfully created our data we need to subclass the control now, if not
        // we could end up in a situation where our data was never freed.
        SetProp(hwnd, DIALOG_DATA_PROPERTY, (HANDLE) this);
        wndProc = (WNDPROC)SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)SizingProc);
    }
    ~DialogData() {
        SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)wndProc);
        RemoveProp(hwnd, DIALOG_DATA_PROPERTY);
        free(this->psd);
    }

    // The number of items contained in the psd member.
    int nItemCount;
    DialogSizerSizingItem *psd;

    // We need the smallest to respond to the WM_GETMINMAXINFO message
    POINT ptSmallest;

    // we need this to decide how much the window has changed size when we get a WM_SIZE message
    SizeI sizeClient;
    bool bMaximised;

    void UpdateGripper() {
        if (!bShowSizingGrip)
            return;

        RectI rcOld = rcGrip;
        UpdateGripperRect();
        // We also need to invalidate the combined area of the old and new rectangles
        // otherwise we would have trail of grippers when we sized the dialog larger
        // in any axis
        RECT tmpRect = rcGrip.Union(rcOld).ToRECT();
        InvalidateRect(hwnd, &tmpRect, TRUE);
    }

    void DrawGripper(HDC hdc) {
        if (bShowSizingGrip && !bMaximised) {
            RECT tmpRect = rcGrip.ToRECT();
            DrawFrameControl(hdc, &tmpRect, DFC_SCROLL, DFCS_SCROLLSIZEGRIP);
        }
    }

    bool InsideGripper(PointI pt) { return bShowSizingGrip && rcGrip.Contains(pt); }

    // The previous window procedure
    WNDPROC wndProc;

  private:
    HWND hwnd;
    RectI rcGrip;
    // Draw the sizing grip...or not
    bool bShowSizingGrip;

    void UpdateGripperRect() {
        int width = GetSystemMetrics(SM_CXVSCROLL);
        int height = GetSystemMetrics(SM_CYHSCROLL);
        rcGrip = RectI(sizeClient.dx - width, sizeClient.dy - height, width, height);
    }
};

// Setting a dialog sizeable involves subclassing the window and handling it's
// WM_SIZE messages.
//
// Returns non-zero for success and zero if it fails
extern "C" BOOL DialogSizer_Set(HWND hwnd, const DialogSizerSizingItem *psd, BOOL bShowSizingGrip) {
    DialogData *pdd = (DialogData *)GetProp(hwnd, DIALOG_DATA_PROPERTY);
    // Overwrite previous settings (if there are any)
    delete pdd;

    pdd = new DialogData(hwnd, psd, bShowSizingGrip);
    if (!pdd || !pdd->psd) {
        delete pdd;
        return FALSE;
    }

    return TRUE;
}

void UpdateWindowSize(DialogData *pdd, const int cx, const int cy, HWND hwnd) {
    const int nDeltaX = cx - pdd->sizeClient.dx;
    const int nDeltaY = cy - pdd->sizeClient.dy;

    HDWP hdwp = BeginDeferWindowPos(pdd->nItemCount);
    for (int i = 0; i < pdd->nItemCount; i++) {
        const DialogSizerSizingItem *psd = pdd->psd + i;
        HWND hwndChild = GetDlgItem(hwnd, psd->uControlID);
        RectI rect = MapRectToWindow(WindowRect(hwndChild), HWND_DESKTOP, hwnd);

        // Adjust the window horizontally
        if (psd->uSizeInfo & DS_MoveX)
            rect.x += nDeltaX;
        // Adjust the window vertically
        if (psd->uSizeInfo & DS_MoveY)
            rect.y += nDeltaY;
        // Size the window horizontally
        if (psd->uSizeInfo & DS_SizeX)
            rect.dx += nDeltaX;
        // Size the window vertically
        if (psd->uSizeInfo & DS_SizeY)
            rect.dy += nDeltaY;

        DeferWindowPos(hdwp, hwndChild, nullptr, rect.x, rect.y, rect.dx, rect.dy,
                       SWP_NOACTIVATE | SWP_NOZORDER);
    }
    EndDeferWindowPos(hdwp);

    pdd->sizeClient = SizeI(cx, cy);
    // If we have a sizing grip enabled then adjust it's position
    pdd->UpdateGripper();
}

// Actual window procedure that will handle saving window size/position and moving
// the controls whilst the window sizes.
static LRESULT CALLBACK SizingProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    DialogData *pdd = (DialogData *)GetProp(hwnd, DIALOG_DATA_PROPERTY);
    if (!pdd)
        return DefWindowProc(hwnd, msg, wParam, lParam);

    switch (msg) {
        case WM_ERASEBKGND: {
            LRESULT lr = CallWindowProc(pdd->wndProc, hwnd, msg, wParam, lParam);
            pdd->DrawGripper((HDC)wParam);
            return lr;
        }

        case WM_SIZE: {
            if (wParam != SIZE_MINIMIZED) {
                pdd->bMaximised = wParam == SIZE_MAXIMIZED;
                UpdateWindowSize(pdd, LOWORD(lParam), HIWORD(lParam), hwnd);
            }
        } break;

        case WM_NCHITTEST: {
            // If the gripper is enabled then perform a simple hit test on our gripper area.
            POINT pt = { LOWORD(lParam), HIWORD(lParam) };
            ScreenToClient(hwnd, &pt);
            if (pdd->InsideGripper(PointI(pt.x, pt.y)))
                return HTBOTTOMRIGHT;
        } break;

        case WM_GETMINMAXINFO: {
            // Our opportunity to say that we do not want the dialog to grow or shrink any more.
            LPMINMAXINFO lpmmi = (LPMINMAXINFO)lParam;
            lpmmi->ptMinTrackSize = pdd->ptSmallest;
        }
            return 0;

        case WM_DESTROY: {
            WNDPROC wndProc = pdd->wndProc;
            delete pdd;
            return CallWindowProc(wndProc, hwnd, msg, wParam, lParam);
        }
    }

    return CallWindowProc(pdd->wndProc, hwnd, msg, wParam, lParam);
}