/*
 * Theming - Scrollbar control
 *
 * Copyright (c) 2015 Mark Harmstone
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 *
 */

#include <stdarg.h>
#include <string.h>
#include <stdlib.h>

#include "windef.h"
#include "winbase.h"
#include "wingdi.h"
#include "winuser.h"
#include "uxtheme.h"
#include "vssym32.h"
#include "comctl32.h"
#include "wine/debug.h"

/* Minimum size of the thumb in pixels */
#define SCROLL_MIN_THUMB 6

/* Minimum size of the rectangle between the arrows */
#define SCROLL_MIN_RECT  4

enum SCROLL_HITTEST
{
    SCROLL_NOWHERE,      /* Outside the scroll bar */
    SCROLL_TOP_ARROW,    /* Top or left arrow */
    SCROLL_TOP_RECT,     /* Rectangle between the top arrow and the thumb */
    SCROLL_THUMB,        /* Thumb rectangle */
    SCROLL_BOTTOM_RECT,  /* Rectangle between the thumb and the bottom arrow */
    SCROLL_BOTTOM_ARROW  /* Bottom or right arrow */
};

static HWND tracking_win = 0;
static enum SCROLL_HITTEST tracking_hot_part = SCROLL_NOWHERE;

WINE_DEFAULT_DEBUG_CHANNEL(theme_scroll);

static void calc_thumb_dimensions(unsigned int size, SCROLLINFO *si, unsigned int *thumbpos, unsigned int *thumbsize)
{
    if (size <= SCROLL_MIN_RECT)
        *thumbpos = *thumbsize = 0;
    else if (si->nPage > si->nMax - si->nMin)
        *thumbpos = *thumbsize = 0;
    else {
        if (si->nPage > 0) {
            *thumbsize = MulDiv(size, si->nPage, si->nMax - si->nMin + 1);
            if (*thumbsize < SCROLL_MIN_THUMB) *thumbsize = SCROLL_MIN_THUMB;
        }
        else *thumbsize = GetSystemMetrics(SM_CXVSCROLL);

        if (size < *thumbsize)
            *thumbpos = *thumbsize = 0;
        else {
            int max = si->nMax - max(si->nPage - 1, 0);
            size -= *thumbsize;
            if (si->nMin >= max)
                *thumbpos = 0;
            else
                *thumbpos = MulDiv(size, si->nTrackPos - si->nMin, max - si->nMin);
        }
    }
}

static enum SCROLL_HITTEST hit_test(HWND hwnd, HTHEME theme, POINT pt)
{
    RECT r;
    DWORD style = GetWindowLongW(hwnd, GWL_STYLE);
    BOOL vertical = style & SBS_VERT;
    SIZE sz;
    SCROLLINFO si;
    unsigned int offset, size, upsize, downsize, thumbpos, thumbsize;

    GetWindowRect(hwnd, &r);
    OffsetRect(&r, -r.left, -r.top);

    if (vertical) {
        offset = pt.y;
        size = r.bottom;

        if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_UPNORMAL, NULL, TS_DRAW, &sz))) {
            WARN("Could not get up arrow size.\n");
            upsize = 0;
        } else
            upsize = sz.cy;

        if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_DOWNNORMAL, NULL, TS_DRAW, &sz))) {
            WARN("Could not get down arrow size.\n");
            downsize = 0;
        } else
            downsize = sz.cy;
    } else {
        offset = pt.x;
        size = r.right;

        if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_LEFTNORMAL, NULL, TS_DRAW, &sz))) {
            WARN("Could not get left arrow size.\n");
            upsize = 0;
        } else
            upsize = sz.cx;

        if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_RIGHTNORMAL, NULL, TS_DRAW, &sz))) {
            WARN("Could not get right arrow size.\n");
            downsize = 0;
        } else
            downsize = sz.cx;
    }

    if (pt.x < 0 || pt.x > r.right || pt.y < 0 || pt.y > r.bottom)
        return SCROLL_NOWHERE;

    if (size < SCROLL_MIN_RECT + upsize + downsize)
        upsize = downsize = (size - SCROLL_MIN_RECT)/2;

    if (offset < upsize)
        return SCROLL_TOP_ARROW;

    if (offset > size - downsize)
        return SCROLL_BOTTOM_ARROW;

    si.cbSize = sizeof(si);
    si.fMask = SIF_ALL;
    if (!GetScrollInfo(hwnd, SB_CTL, &si)) {
        WARN("GetScrollInfo failed.\n");
        return SCROLL_NOWHERE;
    }

    calc_thumb_dimensions(size - upsize - downsize, &si, &thumbpos, &thumbsize);

    if (offset < upsize + thumbpos)
        return SCROLL_TOP_RECT;
    else if (offset < upsize + thumbpos + thumbsize)
        return SCROLL_THUMB;
    else
        return SCROLL_BOTTOM_RECT;
}

static void redraw_part(HWND hwnd, HTHEME theme, enum SCROLL_HITTEST part)
{
    DWORD style = GetWindowLongW(hwnd, GWL_STYLE);
    BOOL vertical = style & SBS_VERT;
    SIZE sz;
    RECT r, partrect;
    unsigned int size, upsize, downsize;

    if (part == SCROLL_NOWHERE) { /* redraw everything */
        InvalidateRect(hwnd, NULL, TRUE);
        return;
    }

    GetWindowRect(hwnd, &r);
    OffsetRect(&r, -r.left, -r.top);

    if (vertical) {
        size = r.bottom;

        if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_UPNORMAL, NULL, TS_DRAW, &sz))) {
            WARN("Could not get up arrow size.\n");
            upsize = 0;
        } else
            upsize = sz.cy;

        if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_DOWNNORMAL, NULL, TS_DRAW, &sz))) {
            WARN("Could not get down arrow size.\n");
            downsize = 0;
        } else
            downsize = sz.cy;
    } else {
        size = r.right;

        if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_LEFTNORMAL, NULL, TS_DRAW, &sz))) {
            WARN("Could not get left arrow size.\n");
            upsize = 0;
        } else
            upsize = sz.cx;

        if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_RIGHTNORMAL, NULL, TS_DRAW, &sz))) {
            WARN("Could not get right arrow size.\n");
            downsize = 0;
        } else
            downsize = sz.cx;
    }

    if (size < SCROLL_MIN_RECT + upsize + downsize)
        upsize = downsize = (size - SCROLL_MIN_RECT)/2;

    partrect = r;

    if (part == SCROLL_TOP_ARROW) {
        if (vertical)
            partrect.bottom = partrect.top + upsize;
        else
            partrect.right = partrect.left + upsize;
    } else if (part == SCROLL_BOTTOM_ARROW) {
        if (vertical)
            partrect.top = partrect.bottom - downsize;
        else
            partrect.left = partrect.right - downsize;
    } else {
        unsigned int thumbpos, thumbsize;
        SCROLLINFO si;

        si.cbSize = sizeof(si);
        si.fMask = SIF_ALL;
        if (!GetScrollInfo(hwnd, SB_CTL, &si)) {
            WARN("GetScrollInfo failed.\n");
            return;
        }

        calc_thumb_dimensions(size - upsize - downsize, &si, &thumbpos, &thumbsize);

        if (part == SCROLL_TOP_RECT) {
            if (vertical) {
                partrect.top = r.top + upsize;
                partrect.bottom = partrect.top + thumbpos;
            } else {
                partrect.left = r.left + upsize;
                partrect.right = partrect.left + thumbpos;
            }
        } else if (part == SCROLL_THUMB) {
            if (vertical) {
                partrect.top = r.top + upsize + thumbpos;
                partrect.bottom = partrect.top + thumbsize;
            } else {
                partrect.left = r.left + upsize + thumbpos;
                partrect.right = partrect.left + thumbsize;
            }
        } else if (part == SCROLL_BOTTOM_RECT) {
            if (vertical) {
                partrect.top = r.top + upsize + thumbpos + thumbsize;
                partrect.bottom = r.bottom - downsize;
            } else {
                partrect.left = r.left + upsize + thumbpos + thumbsize;
                partrect.right = r.right - downsize;
            }
        }
    }

    InvalidateRect(hwnd, &partrect, TRUE);
}

static void scroll_event(HWND hwnd, HTHEME theme, UINT msg, POINT pt)
{
    enum SCROLL_HITTEST hittest;
    TRACKMOUSEEVENT tme;

    if (GetWindowLongW(hwnd, GWL_STYLE) & (SBS_SIZEGRIP | SBS_SIZEBOX))
        return;

    hittest = hit_test(hwnd, theme, pt);

    switch (msg)
    {
        case WM_MOUSEMOVE:
            hittest = hit_test(hwnd, theme, pt);
            tracking_win = hwnd;
            break;

        case WM_MOUSELEAVE:
            if (tracking_win == hwnd) {
                hittest = SCROLL_NOWHERE;
            }
            break;
    }

    tme.cbSize = sizeof(tme);
    tme.dwFlags = TME_QUERY;
    TrackMouseEvent(&tme);

    if (!(tme.dwFlags & TME_LEAVE) || tme.hwndTrack != hwnd) {
        tme.dwFlags = TME_LEAVE;
        tme.hwndTrack = hwnd;
        TrackMouseEvent(&tme);
    }

    if (tracking_win != hwnd && msg == WM_MOUSELEAVE) {
        redraw_part(hwnd, theme, SCROLL_NOWHERE);
        return;
    }

    if (tracking_win == hwnd && hittest != tracking_hot_part) {
        enum SCROLL_HITTEST oldhotpart = tracking_hot_part;

        tracking_hot_part = hittest;

        if (hittest != SCROLL_NOWHERE)
            redraw_part(hwnd, theme, hittest);
        else
            tracking_win = 0;

        if (oldhotpart != SCROLL_NOWHERE)
            redraw_part(hwnd, theme, oldhotpart);
    }
}

static void paint_scrollbar(HWND hwnd, HTHEME theme)
{
    HDC dc;
    PAINTSTRUCT ps;
    RECT r;
    DWORD style = GetWindowLongW(hwnd, GWL_STYLE);
    BOOL vertical = style & SBS_VERT;
    BOOL disabled = !IsWindowEnabled(hwnd);

    GetWindowRect(hwnd, &r);
    OffsetRect(&r, -r.left, -r.top);

    dc = BeginPaint(hwnd, &ps);

    if (style & SBS_SIZEBOX || style & SBS_SIZEGRIP) {
        int state;

        if (style & SBS_SIZEBOXTOPLEFTALIGN)
            state = SZB_TOPLEFTALIGN;
        else
            state = SZB_RIGHTALIGN;

        DrawThemeBackground(theme, dc, SBP_SIZEBOX, state, &r, NULL);
    } else {
        SCROLLINFO si;
        unsigned int thumbpos, thumbsize;
        int uppertrackstate, lowertrackstate, thumbstate;
        RECT partrect, trackrect;
        SIZE grippersize;

        si.cbSize = sizeof(si);
        si.fMask = SIF_ALL;
        GetScrollInfo(hwnd, SB_CTL, &si);

        trackrect = r;

        if (disabled) {
            uppertrackstate = SCRBS_DISABLED;
            lowertrackstate = SCRBS_DISABLED;
            thumbstate = SCRBS_DISABLED;
        } else {
            uppertrackstate = SCRBS_NORMAL;
            lowertrackstate = SCRBS_NORMAL;
            thumbstate = SCRBS_NORMAL;

            if (tracking_win == hwnd) {
                if (tracking_hot_part == SCROLL_TOP_RECT)
                    uppertrackstate = SCRBS_HOT;
                else if (tracking_hot_part == SCROLL_BOTTOM_RECT)
                    lowertrackstate = SCRBS_HOT;
                else if (tracking_hot_part == SCROLL_THUMB)
                    thumbstate = SCRBS_HOT;
            }
        }

        if (vertical) {
            SIZE upsize, downsize;
            int uparrowstate, downarrowstate;

            if (disabled) {
                uparrowstate = ABS_UPDISABLED;
                downarrowstate = ABS_DOWNDISABLED;
            } else {
                uparrowstate = ABS_UPNORMAL;
                downarrowstate = ABS_DOWNNORMAL;

                if (tracking_win == hwnd) {
                    if (tracking_hot_part == SCROLL_TOP_ARROW)
                        uparrowstate = ABS_UPHOT;
                    else if (tracking_hot_part == SCROLL_BOTTOM_ARROW)
                        downarrowstate = ABS_DOWNHOT;
                }
            }

            if (FAILED(GetThemePartSize(theme, dc, SBP_ARROWBTN, uparrowstate, NULL, TS_DRAW, &upsize))) {
                WARN("Could not get up arrow size.\n");
                return;
            }

            if (FAILED(GetThemePartSize(theme, dc, SBP_ARROWBTN, downarrowstate, NULL, TS_DRAW, &downsize))) {
                WARN("Could not get down arrow size.\n");
                return;
            }

            if (r.bottom - r.top - upsize.cy - downsize.cy < SCROLL_MIN_RECT)
                upsize.cy = downsize.cy = (r.bottom - r.top - SCROLL_MIN_RECT)/2;

            partrect = r;
            partrect.bottom = partrect.top + upsize.cy;
            DrawThemeBackground(theme, dc, SBP_ARROWBTN, uparrowstate, &partrect, NULL);

            trackrect.top = partrect.bottom;

            partrect.bottom = r.bottom;
            partrect.top = partrect.bottom - downsize.cy;
            DrawThemeBackground(theme, dc, SBP_ARROWBTN, downarrowstate, &partrect, NULL);

            trackrect.bottom = partrect.top;

            calc_thumb_dimensions(trackrect.bottom - trackrect.top, &si, &thumbpos, &thumbsize);

            if (thumbpos > 0) {
                partrect.top = trackrect.top;
                partrect.bottom = partrect.top + thumbpos;

                DrawThemeBackground(theme, dc, SBP_UPPERTRACKVERT, uppertrackstate, &partrect, NULL);
            }

            if (thumbsize > 0) {
                partrect.top = trackrect.top + thumbpos;
                partrect.bottom = partrect.top + thumbsize;

                DrawThemeBackground(theme, dc, SBP_THUMBBTNVERT, thumbstate, &partrect, NULL);

                if (SUCCEEDED(GetThemePartSize(theme, dc, SBP_GRIPPERVERT, thumbstate, NULL, TS_DRAW, &grippersize))) {
                    MARGINS margins;

                    if (SUCCEEDED(GetThemeMargins(theme, dc, SBP_THUMBBTNVERT, thumbstate, TMT_CONTENTMARGINS, &partrect, &margins))) {
                        if (grippersize.cy <= (thumbsize - margins.cyTopHeight - margins.cyBottomHeight))
                            DrawThemeBackground(theme, dc, SBP_GRIPPERVERT, thumbstate, &partrect, NULL);
                    }
                }
            }

            if (thumbpos + thumbsize < trackrect.bottom - trackrect.top) {
                partrect.bottom = trackrect.bottom;
                partrect.top = trackrect.top + thumbsize + thumbpos;

                DrawThemeBackground(theme, dc, SBP_LOWERTRACKVERT, lowertrackstate, &partrect, NULL);
            }
        } else {
            SIZE leftsize, rightsize;
            int leftarrowstate, rightarrowstate;

            if (disabled) {
                leftarrowstate = ABS_LEFTDISABLED;
                rightarrowstate = ABS_RIGHTDISABLED;
            } else {
                leftarrowstate = ABS_LEFTNORMAL;
                rightarrowstate = ABS_RIGHTNORMAL;

                if (tracking_win == hwnd) {
                    if (tracking_hot_part == SCROLL_TOP_ARROW)
                        leftarrowstate = ABS_LEFTHOT;
                    else if (tracking_hot_part == SCROLL_BOTTOM_ARROW)
                        rightarrowstate = ABS_RIGHTHOT;
                }
            }

            if (FAILED(GetThemePartSize(theme, dc, SBP_ARROWBTN, leftarrowstate, NULL, TS_DRAW, &leftsize))) {
                WARN("Could not get left arrow size.\n");
                return;
            }

            if (FAILED(GetThemePartSize(theme, dc, SBP_ARROWBTN, rightarrowstate, NULL, TS_DRAW, &rightsize))) {
                WARN("Could not get right arrow size.\n");
                return;
            }

            if (r.right - r.left - leftsize.cx - rightsize.cx < SCROLL_MIN_RECT)
                leftsize.cx = rightsize.cx = (r.right - r.left - SCROLL_MIN_RECT)/2;

            partrect = r;
            partrect.right = partrect.left + leftsize.cx;
            DrawThemeBackground(theme, dc, SBP_ARROWBTN, leftarrowstate, &partrect, NULL);

            trackrect.left = partrect.right;

            partrect.right = r.right;
            partrect.left = partrect.right - rightsize.cx;
            DrawThemeBackground(theme, dc, SBP_ARROWBTN, rightarrowstate, &partrect, NULL);

            trackrect.right = partrect.left;

            calc_thumb_dimensions(trackrect.right - trackrect.left, &si, &thumbpos, &thumbsize);

            if (thumbpos > 0) {
                partrect.left = trackrect.left;
                partrect.right = partrect.left + thumbpos;

                DrawThemeBackground(theme, dc, SBP_UPPERTRACKHORZ, uppertrackstate, &partrect, NULL);
            }

            if (thumbsize > 0) {
                partrect.left = trackrect.left + thumbpos;
                partrect.right = partrect.left + thumbsize;

                DrawThemeBackground(theme, dc, SBP_THUMBBTNHORZ, thumbstate, &partrect, NULL);

                if (SUCCEEDED(GetThemePartSize(theme, dc, SBP_GRIPPERHORZ, thumbstate, NULL, TS_DRAW, &grippersize))) {
                    MARGINS margins;

                    if (SUCCEEDED(GetThemeMargins(theme, dc, SBP_THUMBBTNHORZ, thumbstate, TMT_CONTENTMARGINS, &partrect, &margins))) {
                        if (grippersize.cx <= (thumbsize - margins.cxLeftWidth - margins.cxRightWidth))
                            DrawThemeBackground(theme, dc, SBP_GRIPPERHORZ, thumbstate, &partrect, NULL);
                    }
                }
            }

            if (thumbpos + thumbsize < trackrect.right - trackrect.left) {
                partrect.right = trackrect.right;
                partrect.left = trackrect.left + thumbsize + thumbpos;

                DrawThemeBackground(theme, dc, SBP_LOWERTRACKHORZ, lowertrackstate, &partrect, NULL);
            }
        }
    }

    EndPaint(hwnd, &ps);
}

LRESULT CALLBACK THEMING_ScrollbarSubclassProc (HWND hwnd, UINT msg,
                                                WPARAM wParam, LPARAM lParam,
                                                ULONG_PTR dwRefData)
{
    const WCHAR* themeClass = WC_SCROLLBARW;
    HTHEME theme;
    LRESULT result;
    POINT pt;

    TRACE("(%p, 0x%x, %lu, %lu, %lu)\n", hwnd, msg, wParam, lParam, dwRefData);

    switch (msg) {
        case WM_CREATE:
            result = THEMING_CallOriginalClass(hwnd, msg, wParam, lParam);
            OpenThemeData(hwnd, themeClass);
            return result;

        case WM_DESTROY:
            theme = GetWindowTheme(hwnd);
            CloseThemeData(theme);
            return THEMING_CallOriginalClass(hwnd, msg, wParam, lParam);

        case WM_THEMECHANGED:
            theme = GetWindowTheme(hwnd);
            CloseThemeData(theme);
            OpenThemeData(hwnd, themeClass);
            break;

        case WM_SYSCOLORCHANGE:
            theme = GetWindowTheme(hwnd);
            if (!theme) return THEMING_CallOriginalClass(hwnd, msg, wParam, lParam);
            /* Do nothing. When themed, a WM_THEMECHANGED will be received, too,
             * which will do the repaint. */
            break;

        case WM_PAINT:
            theme = GetWindowTheme(hwnd);
            if (!theme) return THEMING_CallOriginalClass(hwnd, msg, wParam, lParam);

            paint_scrollbar(hwnd, theme);
            break;

        case WM_MOUSEMOVE:
        case WM_MOUSELEAVE:
            theme = GetWindowTheme(hwnd);
            if (!theme) return THEMING_CallOriginalClass(hwnd, msg, wParam, lParam);

            pt.x = (short)LOWORD(lParam);
            pt.y = (short)HIWORD(lParam);
            scroll_event(hwnd, theme, msg, pt);
            break;

        default:
            return THEMING_CallOriginalClass(hwnd, msg, wParam, lParam);
    }

    return 0;
}
