/*
 * Copyright (C) 2017 ~ 2017 Deepin Technology Co., Ltd.
 *
 * This program 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 3 of the License, or
 * any later version.
 *
 * This program 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 Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "xutil.h"

#include <QDebug>
#include <QTimer>
#include <QWidget>

#include <QX11Info>

#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/extensions/shape.h>

DWIDGET_BEGIN_NAMESPACE

namespace XUtils
{
// From the WM spec
#define _NET_WM_MOVERESIZE_INVALID          -1
#define _NET_WM_MOVERESIZE_SIZE_TOPLEFT      0
#define _NET_WM_MOVERESIZE_SIZE_TOP          1
#define _NET_WM_MOVERESIZE_SIZE_TOPRIGHT     2
#define _NET_WM_MOVERESIZE_SIZE_RIGHT        3
#define _NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT  4
#define _NET_WM_MOVERESIZE_SIZE_BOTTOM       5
#define _NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT   6
#define _NET_WM_MOVERESIZE_SIZE_LEFT         7
#define _NET_WM_MOVERESIZE_MOVE              8   /* movement only */
#define _NET_WM_MOVERESIZE_SIZE_KEYBOARD     9   /* size via keyboard */
#define _NET_WM_MOVERESIZE_MOVE_KEYBOARD    10   /* move via keyboard */
#define _NET_WM_MOVERESIZE_CANCEL           11   /* cancel operation */

#define _NET_WM_STATE_REMOVE        0    /* remove/unset property */
#define _NET_WM_STATE_ADD           1    /* add/set property */
#define _NET_WM_STATE_TOGGLE        2    /* toggle property  */

const char kAtomNameHidden[] = "_NET_WM_STATE_HIDDEN";
const char kAtomNameFullscreen[] = "_NET_WM_STATE_FULLSCREEN";
const char kAtomNameMaximizedHorz[] = "_NET_WM_STATE_MAXIMIZED_HORZ";
const char kAtomNameMaximizedVert[] = "_NET_WM_STATE_MAXIMIZED_VERT";
const char kAtomNameMoveResize[] = "_NET_WM_MOVERESIZE";
const char kAtomNameWmState[] = "_NET_WM_STATE";
const char kAtomNameWmStateAbove[] = "_NET_WM_STATE_ABOVE";
const char kAtomNameWmStateStaysOnTop[] = "_NET_WM_STATE_STAYS_ON_TOP";
const char kAtomNameWmSkipTaskbar[] = "_NET_WM_STATE_SKIP_TASKBAR";
const char kAtomNameWmSkipPager[] = "_NET_WM_STATE_SKIP_PAGER";

struct MwmHints {
    unsigned long flags;
    unsigned long functions;
    unsigned long decorations;
    long input_mode;
    unsigned long status;
};

enum {
    MWM_HINTS_FUNCTIONS = (1L << 0),
    MWM_HINTS_DECORATIONS = (1L << 1),

    MWM_FUNC_ALL = (1L << 0),
    MWM_FUNC_RESIZE = (1L << 1),
    MWM_FUNC_MOVE = (1L << 2),
    MWM_FUNC_MINIMIZE = (1L << 3),
    MWM_FUNC_MAXIMIZE = (1L << 4),
    MWM_FUNC_CLOSE = (1L << 5),

    MWM_DECOR_ALL      = (1L << 0),
    MWM_DECOR_BORDER   = (1L << 1),
    MWM_DECOR_RESIZEH  = (1L << 2),
    MWM_DECOR_TITLE    = (1L << 3),
    MWM_DECOR_MENU     = (1L << 4),
    MWM_DECOR_MINIMIZE = (1L << 5),
    MWM_DECOR_MAXIMIZE = (1L << 6),

    MWM_HINTS_INPUT_MODE = (1L << 2),

    MWM_INPUT_MODELESS                  = 0L,
    MWM_INPUT_PRIMARY_APPLICATION_MODAL = 1L,
    MWM_INPUT_FULL_APPLICATION_MODAL    = 3L
};

static int CornerEdge2WmGravity(const CornerEdge &ce)
{
    switch (ce) {
    case CornerEdge::kTopLeft:     return _NET_WM_MOVERESIZE_SIZE_TOPLEFT;
    case CornerEdge::kTop:         return _NET_WM_MOVERESIZE_SIZE_TOP;
    case CornerEdge::kTopRight:    return _NET_WM_MOVERESIZE_SIZE_TOPRIGHT;
    case CornerEdge::kRight:       return _NET_WM_MOVERESIZE_SIZE_RIGHT;
    case CornerEdge::kBottomRight: return _NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT;
    case CornerEdge::kBottom:      return _NET_WM_MOVERESIZE_SIZE_BOTTOM;
    case CornerEdge::kBottomLeft:  return _NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT;
    case CornerEdge::kLeft:        return _NET_WM_MOVERESIZE_SIZE_LEFT;
    default:                       return _NET_WM_MOVERESIZE_INVALID;
    }
}

static XCursorType CornerEdge2XCursor(const CornerEdge &ce)
{
    switch (ce) {
    case CornerEdge::kTop:         return XCursorType::kTop;
    case CornerEdge::kTopRight:    return XCursorType::kTopRight;
    case CornerEdge::kRight:       return XCursorType::kRight;
    case CornerEdge::kBottomRight: return XCursorType::kBottomRight;
    case CornerEdge::kBottom:      return XCursorType::kBottom;
    case CornerEdge::kBottomLeft:  return XCursorType::kBottomLeft;
    case CornerEdge::kLeft:        return XCursorType::kLeft;
    case CornerEdge::kTopLeft:     return XCursorType::kTopLeft;
    default:                       return XCursorType::kInvalid;
    }
}

void ChangeWindowMaximizedState(const QWidget *widget, int wm_state)
{
    const auto display = QX11Info::display();
    const auto screen = QX11Info::appScreen();

    XEvent xev;
    memset(&xev, 0, sizeof(xev));
    const Atom net_wm_state = XInternAtom(display, kAtomNameWmState, false);
    const Atom vertical_maximized = XInternAtom(display,
                                    kAtomNameMaximizedVert,
                                    false);
    const Atom horizontal_maximized = XInternAtom(display,
                                      kAtomNameMaximizedHorz,
                                      false);

    xev.xclient.type = ClientMessage;
    xev.xclient.message_type = net_wm_state;
    xev.xclient.display = display;
    xev.xclient.window = widget->winId();
    xev.xclient.format = 32;

    xev.xclient.data.l[0] = wm_state;
    xev.xclient.data.l[1] = vertical_maximized;
    xev.xclient.data.l[2] = horizontal_maximized;
    xev.xclient.data.l[3] = 1;

    XSendEvent(display,
               QX11Info::appRootWindow(screen),
               false,
               SubstructureRedirectMask | SubstructureNotifyMask,
               &xev);
    XFlush(display);
}

CornerEdge GetCornerEdge(const QWidget *widget, int x, int y, const QMargins &margins, int border_width)
{
    QRect fullRect = widget->rect();
    fullRect = fullRect.marginsRemoved(margins);
    unsigned int ce = static_cast<unsigned int>(CornerEdge::kInvalid);
    if ((y - fullRect.top() >= -border_width)
            && (y < fullRect.top())) {
        ce = ce | static_cast<unsigned int>(CornerEdge::kTop);
    }
    if ((x - fullRect.left() >= -border_width)
            && (x < fullRect.left())) {
        ce = ce | static_cast<unsigned int>(CornerEdge::kLeft);
    }
    if ((y - fullRect.bottom() <= border_width)
            && (y > fullRect.bottom())) {
        ce = ce | static_cast<unsigned int>(CornerEdge::kBottom);
    }
    if ((x - fullRect.right() <= border_width)
            && (x > fullRect.right())) {
        ce = ce | static_cast<unsigned int>(CornerEdge::kRight);
    }
    return static_cast<CornerEdge>(ce);
}

void SendMoveResizeMessage(const QWidget *widget, Qt::MouseButton qbutton, int action)
{
    const auto display = QX11Info::display();
    const auto screen = QX11Info::appScreen();

    int xbtn = qbutton == Qt::LeftButton ? Button1 :
               qbutton == Qt::RightButton ? Button3 :
               AnyButton;

    XEvent xev;
    memset(&xev, 0, sizeof(xev));
    const Atom net_move_resize = XInternAtom(display, kAtomNameMoveResize, false);
    xev.xclient.type = ClientMessage;
    xev.xclient.message_type = net_move_resize;
    xev.xclient.display = display;
    xev.xclient.window = widget->winId();
    xev.xclient.format = 32;

    const auto global_position = QCursor::pos();
    xev.xclient.data.l[0] = global_position.x();
    xev.xclient.data.l[1] = global_position.y();
    xev.xclient.data.l[2] = action;
    xev.xclient.data.l[3] = xbtn;
    xev.xclient.data.l[4] = 0;
    XUngrabPointer(display, QX11Info::appTime());

    XSendEvent(display,
               QX11Info::appRootWindow(screen),
               false,
               SubstructureRedirectMask | SubstructureNotifyMask,
               &xev);
    XFlush(display);
}

bool IsCornerEdget(const QWidget *widget, int x, int y, const QMargins &margins, int border_width)
{
    return GetCornerEdge(widget, x, y, margins, border_width) != CornerEdge::kInvalid;
}

void MoveWindow(const QWidget *widget, Qt::MouseButton qbutton)
{
    SendMoveResizeMessage(widget, qbutton, _NET_WM_MOVERESIZE_MOVE);
}

void MoveResizeWindow(const QWidget *widget, Qt::MouseButton qbutton, int x, int y, const QMargins &margins, int border_width)
{
    const CornerEdge ce = GetCornerEdge(widget, x, y, margins, border_width);
    if (ce != CornerEdge::kInvalid) {
        const int action = CornerEdge2WmGravity(ce);
        SendMoveResizeMessage(widget, qbutton, action);
    }
}

void ResetCursorShape(const QWidget *widget)
{
    const auto display = QX11Info::display();
    const WId window_id = widget->winId();
    XUndefineCursor(display, window_id);
    XFlush(display);
}

bool SetCursorShape(const QWidget *widget, int cursor_id)
{
    const auto display = QX11Info::display();
    const WId window_id = widget->winId();
    const Cursor cursor = XCreateFontCursor(display, cursor_id);
    if (!cursor) {
        qWarning() << "[ui]::SetCursorShape() call XCreateFontCursor() failed";
        return false;
    }
    const int result = XDefineCursor(display, window_id, cursor);
    XFlush(display);
    return result == Success;
}

void ShowFullscreenWindow(const QWidget *widget, bool is_fullscreen)
{
    const auto display = QX11Info::display();
    const auto screen = QX11Info::appScreen();

    XEvent xev;
    memset(&xev, 0, sizeof(xev));
    const Atom net_wm_state = XInternAtom(display, kAtomNameWmState, false);
    const Atom fullscreen = XInternAtom(display, kAtomNameFullscreen, false);
    xev.xclient.type = ClientMessage;
    xev.xclient.message_type = net_wm_state;
    xev.xclient.display = display;
    xev.xclient.window = widget->winId();
    xev.xclient.format = 32;

    if (is_fullscreen) {
        xev.xclient.data.l[0] = _NET_WM_STATE_ADD;
    } else {
        xev.xclient.data.l[0] = _NET_WM_STATE_REMOVE;
    }
    xev.xclient.data.l[1] = fullscreen;
    xev.xclient.data.l[2] = 0;
    xev.xclient.data.l[3] = 1;

    XSendEvent(display,
               QX11Info::appRootWindow(screen),
               false,
               SubstructureRedirectMask | SubstructureNotifyMask,
               &xev
              );
    XFlush(display);
}

void ShowMaximizedWindow(const QWidget *widget)
{
    ChangeWindowMaximizedState(widget, _NET_WM_STATE_ADD);
}

void ShowMinimizedWindow(const QWidget *widget, bool minimized)
{
    const auto display = QX11Info::display();
    const auto screen = QX11Info::appScreen();

    XEvent xev;
    memset(&xev, 0, sizeof(xev));
    const Atom net_wm_state = XInternAtom(display, kAtomNameWmState, false);
    const Atom hidden = XInternAtom(display, kAtomNameHidden, false);
    xev.xclient.type = ClientMessage;
    xev.xclient.message_type = net_wm_state;
    xev.xclient.display = display;
    xev.xclient.window = widget->winId();
    xev.xclient.format = 32;

    if (minimized) {
        xev.xclient.data.l[0] = _NET_WM_STATE_ADD;
    } else {
        xev.xclient.data.l[0] = _NET_WM_STATE_REMOVE;
    }
    xev.xclient.data.l[1] = hidden;
    xev.xclient.data.l[2] = 0;
    xev.xclient.data.l[3] = 1;

    XSendEvent(display,
               QX11Info::appRootWindow(screen),
               false,
               SubstructureRedirectMask | SubstructureNotifyMask,
               &xev
              );

    XIconifyWindow(display, widget->winId(), screen);
    XFlush(display);
}

void ShowNormalWindow(const QWidget *widget)
{
    ChangeWindowMaximizedState(widget, _NET_WM_STATE_REMOVE);
}

void ToggleMaximizedWindow(const QWidget *widget)
{
    ChangeWindowMaximizedState(widget, _NET_WM_STATE_TOGGLE);
}

bool UpdateCursorShape(const QWidget *widget, int x, int y, const QMargins &margins, int border_width)
{
    const CornerEdge ce = GetCornerEdge(widget, x, y, margins, border_width);
    const XCursorType x_cursor = CornerEdge2XCursor(ce);
    if (x_cursor != XCursorType::kInvalid) {
        return SetCursorShape(widget, static_cast<unsigned int>(x_cursor));
    } else {
        ResetCursorShape(widget);
        return false;
    }
}

void SkipTaskbarPager(const QWidget *widget)
{
    Q_ASSERT(widget);

    const auto display = QX11Info::display();
    const auto screen = QX11Info::appScreen();

    const auto wmStateAtom = XInternAtom(display, kAtomNameWmState, false);
    const auto taskBarAtom = XInternAtom(display, kAtomNameWmSkipTaskbar, false);
    const auto noPagerAtom = XInternAtom(display, kAtomNameWmSkipPager, false);

    XEvent xev;
    memset(&xev, 0, sizeof(xev));

    xev.xclient.type = ClientMessage;
    xev.xclient.message_type = wmStateAtom;
    xev.xclient.display = display;
    xev.xclient.window = widget->winId();;
    xev.xclient.format = 32;

    xev.xclient.data.l[0] = _NET_WM_STATE_ADD;
    xev.xclient.data.l[1] = taskBarAtom;
    xev.xclient.data.l[2] = noPagerAtom;
    xev.xclient.data.l[3] = 1;

    XSendEvent(display,
               QX11Info::appRootWindow(screen),
               false,
               SubstructureRedirectMask | SubstructureNotifyMask,
               &xev);
    XFlush(display);
}

void SetStayOnTop(const QWidget *widget, bool on)
{
    Q_ASSERT(widget);

    const auto display = QX11Info::display();
    const auto screen = QX11Info::appScreen();

    const auto wmStateAtom = XInternAtom(display, kAtomNameWmState, false);
    const auto stateAboveAtom = XInternAtom(display, kAtomNameWmStateAbove, false);
    const auto stateStaysOnTopAtom = XInternAtom(display,
                                     kAtomNameWmStateStaysOnTop,
                                     false);

    XEvent xev;
    memset(&xev, 0, sizeof(xev));

    xev.xclient.type = ClientMessage;
    xev.xclient.message_type = wmStateAtom;
    xev.xclient.display = display;
    xev.xclient.window = widget->winId();
    xev.xclient.format = 32;

    xev.xclient.data.l[0] = on ? _NET_WM_STATE_ADD : _NET_WM_STATE_REMOVE;
    xev.xclient.data.l[1] = stateAboveAtom;
    xev.xclient.data.l[2] = stateStaysOnTopAtom;
    xev.xclient.data.l[3] = 1;

    XSendEvent(display,
               QX11Info::appRootWindow(screen),
               false,
               SubstructureRedirectMask | SubstructureNotifyMask,
               &xev);
    XFlush(display);
}

void SetMouseTransparent(const QWidget *widget, bool on)
{
    Q_ASSERT(widget);

    const auto display = QX11Info::display();
    XRectangle XRect;
    XRect.x = 0;
    XRect.y = 0;
    int nRects = 0;

    if (on) {
        XRect.width = 0;
        XRect.height = 0;
        nRects = 0;
    } else {
        XRect.width = widget->width();
        XRect.height = widget->height();
        nRects = 1;
    }
    XShapeCombineRectangles(display, widget->winId(), ShapeInput,
                            0, 0,
                            &XRect, nRects, ShapeSet, YXBanded);
}

void SetWindowExtents(const QWidget *widget, const QMargins &margins, const int resizeHandleWidth)
{
    SetWindowExtents(widget->winId(), widget->rect(), margins, resizeHandleWidth);
}


void PropagateSizeHints(const QWidget *w)
{
    const auto display = QX11Info::display();
    XSizeHints *sh = XAllocSizeHints();
    sh->flags = PPosition | PSize | PMinSize | PMaxSize | PResizeInc;
    sh->x = w->x();
    sh->y = w->y();
    sh->min_width = w->minimumWidth();
    sh->min_height = w->minimumHeight();
    sh->base_width = w->baseSize().width();
    sh->base_height = w->baseSize().height();
    sh->max_width = w->maximumWidth();
    sh->max_height = w->maximumHeight();
    sh->width_inc =  w->sizeIncrement().width();
    sh->height_inc = w->sizeIncrement().height();
    XSetWMNormalHints(display, w->winId(), sh);
    XFree(sh);
}

void DisableResize(const QWidget *w)
{
    Display *display = QX11Info::display();
    Atom mwmHintsProperty = XInternAtom(display, "_MOTIF_WM_HINTS", 0);
    struct MwmHints *hints;
    unsigned char *wm_data;
    Atom wm_type;
    int wm_format;
    unsigned long wm_nitems, wm_bytes_after;

    XGetWindowProperty(display,
                       w->winId(),
                       mwmHintsProperty,
                       0,
                       sizeof(MwmHints) / sizeof(long),
                       false,
                       AnyPropertyType,
                       &wm_type,
                       &wm_format,
                       &wm_nitems,
                       &wm_bytes_after,
                       &wm_data);

    hints = (MwmHints *) wm_data;

    hints->flags |= MWM_HINTS_FUNCTIONS;
    if (hints->functions == MWM_FUNC_ALL) {
        hints->functions = MWM_FUNC_MOVE | MWM_FUNC_MINIMIZE | MWM_FUNC_CLOSE;
    } else {
        hints->functions &= ~MWM_FUNC_RESIZE;
        hints->functions |= MWM_FUNC_CLOSE;
    }

    if (hints->decorations == MWM_DECOR_ALL) {
        hints->flags |= MWM_HINTS_DECORATIONS;
        hints->decorations = (MWM_DECOR_BORDER
                              | MWM_DECOR_TITLE
                              | MWM_DECOR_MENU);
    } else {
        hints->decorations &= ~MWM_DECOR_RESIZEH;
    }
    XChangeProperty(display,
                    w->winId(),
                    mwmHintsProperty,
                    mwmHintsProperty,
                    32,
                    PropModeReplace,
                    (unsigned char *)hints,
                    5);
}

void StartResizing(const QWidget *w, const QPoint &globalPoint, const CornerEdge &ce)
{
    const auto display = QX11Info::display();
    const auto winId = w->winId();
    const auto screen = QX11Info::appScreen();

    XEvent xev;
    const Atom netMoveResize = XInternAtom(display, "_NET_WM_MOVERESIZE", false);
    xev.xclient.type = ClientMessage;
    xev.xclient.message_type = netMoveResize;
    xev.xclient.display = display;
    xev.xclient.window = winId;
    xev.xclient.format = 32;

    xev.xclient.data.l[0] = globalPoint.x();
    xev.xclient.data.l[1] = globalPoint.y();
    xev.xclient.data.l[2] = CornerEdge2WmGravity(ce);
    xev.xclient.data.l[3] = Button1;
    xev.xclient.data.l[4] = 1;
    XUngrabPointer(display, QX11Info::appTime());

    XSendEvent(display,
               QX11Info::appRootWindow(screen),
               false,
               SubstructureRedirectMask | SubstructureNotifyMask,
               &xev);
    XFlush(display);
}

void CancelMoveWindow(const QWidget *widget, Qt::MouseButton qbutton)
{
    SendMoveResizeMessage(widget, qbutton, _NET_WM_MOVERESIZE_CANCEL);
}

void SetWindowExtents(uint wid, const QRect &windowRect, const QMargins &margins, const int resizeHandleSize)
{
    Atom frameExtents;
    unsigned long value[4] = {
        (unsigned long)(margins.left()),
        (unsigned long)(margins.right()),
        (unsigned long)(margins.top()),
        (unsigned long)(margins.bottom())
    };
    frameExtents = XInternAtom(QX11Info::display(), "_GTK_FRAME_EXTENTS", False);
    if (frameExtents == None) {
        qWarning() << "Failed to create atom with name DEEPIN_WINDOW_SHADOW";
        return;
    }
    XChangeProperty(QX11Info::display(),
                    wid,
                    frameExtents,
                    XA_CARDINAL,
                    32,
                    PropModeReplace,
                    (unsigned char *)value,
                    4);

    QRect tmp_rect = windowRect;

    tmp_rect -= margins;

    XRectangle contentXRect;
    contentXRect.x = 0;
    contentXRect.y = 0;
    contentXRect.width = tmp_rect.width() + resizeHandleSize * 2;
    contentXRect.height = tmp_rect.height() + resizeHandleSize * 2;
    XShapeCombineRectangles(QX11Info::display(),
                            wid,
                            ShapeInput,
                            margins.left() - resizeHandleSize,
                            margins.top() - resizeHandleSize,
                            &contentXRect, 1, ShapeSet, YXBanded);
}

}

DWIDGET_END_NAMESPACE
