/*
 * QT Client for X2GoKDrive
 * Copyright (C) 2018-2023 Oleksandr Shneyder <o.shneyder@phoca-gmbh.de>
 * Copyright (C) 2018-2023 phoca-GmbH
 *
 * This program 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.
 *
 * 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 General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
#include "extwin.h"
#include "displayarea.h"
#include "client.h"
#include <QApplication>
#include <QTimer>
#include <QCloseEvent>
#include <QScreen>
#ifdef Q_OS_WIN
#include <dwmapi.h>
#endif
ExtWin::ExtWin(uint32_t extWinId, Client *client, QWidget* parent, uint8_t wt, Qt::WindowFlags flags): QMainWindow(parent, flags)
{
    changeTimer=new QTimer(this);
    changeTimer->setSingleShot(true);
    connect(changeTimer, SIGNAL(timeout()), this, SLOT(slotChangeWin()));
    displayArea=new DisplayArea((Client*)client,this);
    displayArea->setObjectName("DisplayArea");
//     displayArea->setStyleSheet("QFrame#DisplayArea{background-color:black;}");
    displayArea->show();
    setCentralWidget(displayArea);
    setWindowIcon(QIcon(":/img/png/x2goclient.png"));
    this->extWinId=extWinId;
    this->client=client;
    windowType=wt;
#ifdef Q_OS_WIN
    if((windowType!=WINDOW_TYPE_NORMAL) && (windowType!=WINDOW_TYPE_DIALOG))
        QTimer::singleShot(10,this,SLOT(slotSetTaskbar()));
    else
        QTimer::singleShot(10,this,SLOT(slotSetWinWindow()));
#endif
#ifdef Q_OS_LINUX
    switch(windowType)
    {
        case WINDOW_TYPE_DROPDOWN_MENU:
            setAttribute(Qt::WA_X11NetWmWindowTypeDropDownMenu);
            break;
        case WINDOW_TYPE_POPUP_MENU:
            setAttribute(Qt::WA_X11NetWmWindowTypePopupMenu);
            break;
        case WINDOW_TYPE_DIALOG:
            setAttribute(Qt::WA_X11NetWmWindowTypeDialog);
            break;
        case WINDOW_TYPE_TOOLTIP:
            setAttribute(Qt::WA_X11NetWmWindowTypeToolTip);
            break;
        case WINDOW_TYPE_UTILITY:
            setAttribute(Qt::WA_X11NetWmWindowTypeUtility);
            break;
    }
#endif
    setFocusPolicy(Qt::NoFocus);
}

#ifdef Q_OS_WIN
void ExtWin::slotSetTaskbar()
{
    HWND hWnd=(HWND)winId();
//     Client::KDRStdErr()<<"HWND "<<hWnd<<KDR_ENDL;
    if(hWnd)
    {
        long style= GetWindowLong(hWnd, GWL_EXSTYLE);
        style |= WS_EX_TOOLWINDOW;
        SetWindowLong(hWnd, GWL_EXSTYLE, style);
    }
}

void ExtWin::slotSetWinWindow()
{
    HWND hWnd=(HWND)winId();
    BOOL fDisable = TRUE;
    DwmSetWindowAttribute(hWnd,
                             DWMWA_TRANSITIONS_FORCEDISABLED,
                             &fDisable,
                             sizeof(fDisable));
}
#endif

void ExtWin::setWinSize(int w, int h)
{
    displayArea->resize(w,h);
}

void ExtWin::moveWinFromServer(QPoint p)
{
    ignoreGeometryChangeEvent=true;
    move(p);
    ignoreGeometryChangeEvent=false;
}

void ExtWin::slotChangeWin()
{
    client->changeWindow(this);
}


void ExtWin::moveEvent(QMoveEvent* ev)
{
    if(((windowType == WINDOW_TYPE_NORMAL)||(windowType == WINDOW_TYPE_DIALOG))&&(!ignoreGeometryChangeEvent && ! isChanging))
    {
#ifdef Q_OS_WIN
        slotChangeWin();
#else
        changeTimer->start(50);
#endif
    }
    QMainWindow::moveEvent(ev);
}

void ExtWin::closeEvent(QCloseEvent* ev)
{
    if((windowType == WINDOW_TYPE_NORMAL)||(windowType == WINDOW_TYPE_DIALOG))
    {
//         Client::KDRStdErr()<<"Request window close ";
        client->changeWindow(this,DELETED);
        ev->ignore();
    }
}

void ExtWin::resizeEvent(QResizeEvent* ev)
{
    if(((windowType == WINDOW_TYPE_NORMAL)||(windowType == WINDOW_TYPE_DIALOG))&&!isChanging)
    {
#ifdef Q_OS_WIN
        displayArea->hide();
        setWindowOpacity(0.2);
#endif
         slotChangeWin();
         slotChangeWin();
#ifdef Q_OS_WIN
         QTimer::singleShot(200, this, SLOT(slotRestoreWin()));
#endif
    }
    QMainWindow::resizeEvent(ev);
}

void ExtWin::showEvent(QShowEvent *ev)
{
    QMainWindow::showEvent(ev);
    if((windowType == WINDOW_TYPE_NORMAL)||(windowType == WINDOW_TYPE_DIALOG))
    {
//         Client::KDRStdErr()<<"Show "<<Client::QRectToStr(geometry())<<KDR_ENDL;
        foreach(ExtWin* w, client->getExtWindows())
        {
            if(w->transWinId==extWinId)
                w->raise();
        }
        QTimer::singleShot(5,this,SLOT(slotCheckStackOrder()));
    }
}

void ExtWin::hideEvent(QHideEvent *ev)
{
    QMainWindow::hideEvent(ev);
    if(((windowType == WINDOW_TYPE_NORMAL)||(windowType == WINDOW_TYPE_DIALOG))&&isMinimized())
    {
//         Client::KDRStdErr()<<"Request window minimize";
        client->changeWindow(this,ICONIFIED);
        foreach(ExtWin* w, client->getExtWindows())
        {
            if(w->transWinId==extWinId)
                w->showMinimized();
            if(w->extWinId==transWinId)
                w->showMinimized();
        }
    }
}

void ExtWin::focusInEvent(QFocusEvent *ev)
{
    Client::KDRStdErr()<<"Focus In "<<Client::QRectToStr(geometry())<<KDR_ENDL;
    QMainWindow::focusInEvent(ev);
}

void ExtWin::focusOutEvent(QFocusEvent *ev)
{
    Client::KDRStdErr()<<"Focus Out "<<Client::QRectToStr(geometry())<<KDR_ENDL;
    QMainWindow::focusOutEvent(ev);
}

#ifdef Q_OS_LINUX
xcb_window_t getRootNativeId(xcb_window_t winId)
{
    xcb_query_tree_cookie_t cookie;
    xcb_query_tree_reply_t *reply;
    xcb_window_t root_id=0;
    xcb_connection_t *connection = QX11Info::connection();
    cookie = xcb_query_tree(connection, winId);
    if ((reply = xcb_query_tree_reply(connection, cookie, NULL)))
    {
        root_id=reply->root;
        free(reply);
    }
    return root_id;
}

xcb_window_t getParentNativeId(xcb_window_t winId)
{
    xcb_query_tree_cookie_t cookie;
    xcb_query_tree_reply_t *reply;
    xcb_window_t parent_id=0;
    xcb_connection_t *connection = QX11Info::connection();
    cookie = xcb_query_tree(connection, winId);
    if ((reply = xcb_query_tree_reply(connection, cookie, NULL)))
    {
        parent_id=reply->parent;
        free(reply);
    }
    return parent_id;
}

xcb_window_t getCommonParent(xcb_window_t win1, xcb_window_t win2)
{
    QList<xcb_window_t> win1_preds;
    xcb_window_t root=getRootNativeId(win1);
    xcb_window_t w_next_pred=getParentNativeId(win1);
    while(w_next_pred!=0)
    {
        win1_preds<<w_next_pred;
        if(w_next_pred==root)
            break;
        w_next_pred=getParentNativeId(w_next_pred);
    }
    //now we have all preds of w1
    w_next_pred=getParentNativeId(win2);
    while(w_next_pred!=0)
    {
        if(win1_preds.indexOf(w_next_pred)!=-1)
        {
            return w_next_pred;
        }
        w_next_pred=getParentNativeId(w_next_pred);
    }
    return 0;
}

xcb_window_t getTopWinId(xcb_window_t myId, xcb_window_t commonParentId)
{
    xcb_window_t parent_id=getParentNativeId(myId);
    while(parent_id!=commonParentId)
    {
        myId=parent_id;
        parent_id=getParentNativeId(myId);
    }
    return myId;
}

ExtWin* ExtWin::findWinByTopWinId(xcb_window_t topWinId, QList<ExtWin*> &siblings)
{
    for(int i=0; i< siblings.count(); ++i)
    {
        if(siblings[i]->topWinId==topWinId)
        {
            return siblings[i];
        }
    }
    return 0;
}

#endif //Q_OS_LINUX
#ifdef Q_OS_WIN
ExtWin* ExtWin::findWinByHWND(HWND hwnd, QList<ExtWin*> &siblings)
{
    for(int i=0; i< siblings.count(); ++i)
    {
        if((HWND)(siblings[i]->winId())==hwnd)
        {
            return siblings[i];
        }
    }
    return 0;
}
#endif

ExtWin* ExtWin::findWinByZInd(int zInd, QList<ExtWin*> &siblings)
{
    for(int i=0; i< siblings.count(); ++i)
    {
        if(siblings[i]->zOrderInd==zInd)
        {
            return siblings[i];
        }
    }
    return 0;
}

QPoint ExtWin::virtualToKdrivePosition(QPoint virtPos)
{
    QRect avGeom=((QGuiApplication*)QGuiApplication::instance())->screens()[0]->availableVirtualGeometry();
    return QPoint(virtPos.x()-avGeom.x(),virtPos.y()-avGeom.y());
}

QPoint ExtWin::kdriveToVirtualPosition(QPoint kdrPos)
{
    QRect avGeom=((QGuiApplication*)QGuiApplication::instance())->screens()[0]->availableVirtualGeometry();
    return QPoint(kdrPos.x()+avGeom.x(),kdrPos.y()+avGeom.y());
}

void ExtWin::slotCheckStackOrder()
{
    #ifdef Q_OS_WIN
    if(!isVisible())
        return;
    #endif //Q_OS_WIN
    QList<ExtWin*> siblings=client->getSiblings(this);
//     Client::KDRStdErr()<<"Have Siblings: "<<siblings.count()<<KDR_ENDL;
    if(siblings.count())
    {
        siblings<<this;
        #ifdef Q_OS_LINUX
        xcb_window_t commonParent=getCommonParent(this->winId(), siblings[0]->winId());
        //             Client::KDRStdErr()<<"common parent: "<<KDR_HEX<<commonParent<<KDR_ENDL;
        xcb_connection_t *connection = QX11Info::connection();
        xcb_query_tree_cookie_t cookie;
        xcb_query_tree_reply_t *reply;
        for(int i=0;i<siblings.count();++i)
            siblings[i]->topWinId=getTopWinId(siblings[i]->winId(),commonParent);
        cookie = xcb_query_tree(connection, commonParent);
        if ((reply = xcb_query_tree_reply(connection, cookie, NULL)))
        {
            xcb_window_t *children = xcb_query_tree_children(reply);
        #endif //Q_OS_LINUX
            int z=0;
            int negative_z=0;
            //                 Client::KDRStdErr()<<"Current stack for parent: "<<KDR_ENDL;
        #ifdef Q_OS_LINUX
            for (int i = 0; i < xcb_query_tree_children_length(reply); i++)
            {
                ExtWin* win=findWinByTopWinId(children[i], siblings);
        #endif //Q_OS_LINUX
        #ifdef Q_OS_WIN
            HWND w=GetTopWindow(NULL);
            while (w)
            {
                ExtWin* win=findWinByHWND(w, siblings);
                w=GetNextWindow(w,GW_HWNDNEXT);
        #endif //Q_OS_WIN
                if(win)
                {
                    if(!win->isMinimized())
                        win->zOrderInd=z++;
                    else
                        win->zOrderInd=--negative_z;
//                     Client::KDRStdErr()<<win->zOrderInd<<" - "<<KDR_HEX<< win->extWinId<<" "<<win->windowTitle()<<KDR_ENDL;
                }
            }
#ifdef Q_OS_WIN
            //need to invert z-order for visible windows (to make it like in Linux)
            for(int i=0;i<siblings.count();++i)
            {
                if(siblings[i]->zOrderInd>=0)
                {
                    siblings[i]->zOrderInd=(z-1)-siblings[i]->zOrderInd;
                }
//                 Client::KDRStdErr()<<siblings[i]->zOrderInd<<" - "<<KDR_HEX<< siblings[i]->extWinId<<" "<<siblings[i]->windowTitle()<<KDR_ENDL;
            }

#endif //Q_OS_WIN
            #ifdef Q_OS_LINUX
            free(reply);
            #endif //Q_OS_LINUX
            //                 Client::KDRStdErr()<<"End of stack "<<KDR_ENDL;
            uint32_t newNextSib;
            ExtWin* win=findWinByZInd(zOrderInd-1, siblings);
            if(!win)
            {
                newNextSib=0;
            }
            else
            {
                newNextSib=win->extWinId;
            }
            if(nextSibId != newNextSib)
            {
                //if some modal windows are not ordered correct, fix it and try again
                if(!checkModality(siblings))
                {
                    QTimer::singleShot(5,this,SLOT(slotCheckStackOrder()));
                    return ;
                }
//                 Client::KDRStdErr()<<"Need to restack "<<KDR_HEX<<extWinId<<" Above "<<newNextSib<<KDR_ENDL;
                client->repaintAllWindows();
                nextSibId=newNextSib;
                client->changeWindow(this);
            }
        }
#ifdef Q_OS_LINUX
    }
#endif
}

bool ExtWin::checkModality(QList<ExtWin*> &siblings)
{
    if(isMinimized())
        return true;
    bool mod_res=true;

    for(int i=0;i<siblings.count();++i)
    {
        ExtWin* w=siblings[i];
        if(w->modality==MOD_SINGLE)
        {
            if(w->transWinId)
            {
                ExtWin* trwin=client->findExtWinById(w->transWinId);
                if(!trwin)
                {
                    Client::KDRStdErr()<<"Error finding trans window  "<<w->transWinId<<KDR_ENDL;
                }
                else
                {
                    if(trwin->zOrderInd > w->zOrderInd)
                    {
                        Client::KDRStdErr()<<"Transwin "<<KDR_HEX<<w->transWinId<< " on top of modal win "<<w->extWinId<<KDR_ENDL;
//                         QTimer::singleShot(1,trwin,SLOT(update()));
//                         QTimer::singleShot(2,w,SLOT(update()));
//                         trwin->update();
                        w->raise();
                        w->activateWindow();
                        mod_res=false;
                    }
                }
            }
        }
    }

    return mod_res;
}

#ifdef Q_OS_WIN
void ExtWin::slotRestoreWin()
{
    displayArea->show();
    setWindowOpacity(1);
    //try to repaint the window to avoid update errors on Windows
    QTimer::singleShot(200, getDisplayArea(), SLOT(repaint()));
}
#endif

bool ExtWin::nativeEvent(const QByteArray &eventType, void *message, long *result)
{
#ifdef Q_OS_LINUX
    if((windowType == WINDOW_TYPE_NORMAL)||(windowType == WINDOW_TYPE_DIALOG))
    {
        if(eventType=="xcb_generic_event_t" && parentId==0)
        {
            xcb_generic_event_t* ev = static_cast<xcb_generic_event_t *>(message);
            switch((ev->response_type & ~0x80))
            {
                case XCB_FOCUS_IN:
                    slotCheckStackOrder();
//                     Client::KDRStdErr()<<"FOCUS IN "<<KDR_HEX<<extWinId<<KDR_ENDL;
                    if(!hasFocus())
                    {
                        setHasFocus(true);
                        client->changeWindow(this);
                    }
                    break;
                case XCB_FOCUS_OUT:
                    slotCheckStackOrder();
//                     Client::KDRStdErr()<<"FOCUS OUT "<<KDR_HEX<<extWinId<<KDR_ENDL;
                    if(hasFocus())
                    {
                        setHasFocus(false);
                        client->changeWindow(this);
                    }
                    break;
            }
        }
    }
#endif //Q_OS_LINUX
#ifdef Q_OS_WIN
    if((windowType == WINDOW_TYPE_NORMAL)||(windowType == WINDOW_TYPE_DIALOG))
    {
        if(eventType=="windows_generic_MSG" && parentId==0)
        {
            MSG* ev = static_cast<MSG *>(message);
            switch((ev->message))
            {
                case WM_ENTERSIZEMOVE:
                    isChanging=true;
                    displayArea->hide();
                    setWindowOpacity(0.2);
                    break;
                case WM_EXITSIZEMOVE:
                    isChanging=false;
                    slotChangeWin();
                    QTimer::singleShot(10, this, SLOT(slotChangeWin()));
                    QTimer::singleShot(200, this, SLOT(slotRestoreWin()));
                    break;
                case WM_SETFOCUS:
                    slotCheckStackOrder();
//                     Client::KDRStdErr()<<"FOCUS IN "<<KDR_HEX<<extWinId<<KDR_ENDL;
                    if(!hasFocus())
                    {
                        setHasFocus(true);
                        client->changeWindow(this);
                    }
                    //try to repaint the window to avoid update errors on Windows
//                     Client::KDRStdErr()<<KDR_ENDL<<time(NULL)<<" SET FOCUS UPDATE "<<KDR_ENDL;
                    QTimer::singleShot(200, getDisplayArea(), SLOT(repaint()));
                    break;
                case WM_KILLFOCUS:
                    slotCheckStackOrder();
//                     if(windowType==WINDOW_TYPE_NORMAL)
                    {
//                        Client::KDRStdErr()<<"FOCUS OUT "<<KDR_HEX<<extWinId<<KDR_ENDL;
//                         QTimer::singleShot(100, this, SLOT(update()));
                    }
                    if(hasFocus())
                    {
                        setHasFocus(false);
                        client->changeWindow(this);
                    }
                    //try to repaint the window to avoid update errors on Windows
//                     Client::KDRStdErr()<<KDR_ENDL<<time(NULL)<<" KILL FOCUS UPDATE "<<KDR_ENDL;
                    QTimer::singleShot(200, getDisplayArea(), SLOT(repaint()));
                    break;
                case WM_PAINT:
                    if(windowType==WINDOW_TYPE_NORMAL)
                    {
//                         Client::KDRStdErr()<<"WM PAINT EVENT: "<<KDR_ENDL;
                    }
                break;
            }
        }
    }
#endif
    return QMainWindow::nativeEvent(eventType, message, result);
}
