/****************************************************************************
**
** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** No Commercial Usage
** This file contains pre-release code and may not be distributed.
** You may use this file in accordance with the terms and conditions
** contained in the Technology Preview License Agreement accompanying
** this package.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights.  These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** If you have questions regarding the use of this file, please contact
** Nokia at qt-info@nokia.com.
**
**
**
**
**
**
**
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "windowmanager.h"
#include <QtCore/QTime>
#include <QtCore/QThread>
#include <QtCore/QDebug>
#include <QtCore/QTextStream>

#ifdef Q_WS_X11
#  include <string.h>     // memset
#  include <X11/Xlib.h>
#  include <X11/Xatom.h>  // XA_WM_STATE
#  include <X11/Xutil.h>
#  include <X11/Xmd.h>    // CARD32
#endif

#if defined(Q_OS_WIN) && !defined(Q_OS_WINCE)
#  include <windows.h>
#endif

// Export the sleep function
class FriendlySleepyThread : public QThread {
public:
    static void sleepMS(int milliSeconds) { msleep(milliSeconds); }
};

#ifdef Q_WS_X11
// X11 Window manager

// Register our own error handler to prevent the defult crashing
// behaviour. It simply counts errors in global variables that
// can be checked after calls.

static unsigned x11ErrorCount = 0;
static const char *currentX11Function = 0;

int xErrorHandler(Display *, XErrorEvent *e)
{
    x11ErrorCount++;

    QString msg;
    QTextStream str(&msg);
    str << "An X11 error (#" << x11ErrorCount<< ") occurred: ";
    if (currentX11Function)
        str << ' ' << currentX11Function << "()";
    str << " code: " << e->error_code;
    str.setIntegerBase(16);
    str << " resource: 0x" << e->resourceid;
    qWarning("%s", qPrintable(msg));

    return 0;
}

static bool isMapped(Display *display, Atom xa_wm_state, Window window, bool *isMapped)
{   
    Atom actual_type;
    int actual_format;
    unsigned long nitems;
    unsigned long bytes_after;
    unsigned char *prop;

    *isMapped = false;
    currentX11Function = "XGetWindowProperty";
    const int retv = XGetWindowProperty(display, window, xa_wm_state, 0L, 1L, False, xa_wm_state,
                                        &actual_type, &actual_format, &nitems, &bytes_after, &prop);

    if (retv != Success || actual_type == None || actual_type != xa_wm_state
        || nitems != 1 || actual_format != 32)
        return false;

    const CARD32 state = * reinterpret_cast<CARD32 *>(prop);

    switch ((int) state) {
    case WithdrawnState:
        *isMapped = false;
        break;
    case NormalState:
    case IconicState:
        *isMapped = true;
        break;
    default:
        *isMapped = true;
        break;
    }
    return true;
}

// Wait until a X11 top level has been mapped, courtesy of xtoolwait.
static Window waitForTopLevelMapped(Display *display, unsigned count, int timeOutMS, QString * errorMessage)
{
    unsigned mappingsCount = count;
    Atom xa_wm_state;
    XEvent event;

    // Discard all pending events
    currentX11Function = "XSync";
    XSync(display, True);

    // Listen for top level creation
    currentX11Function = "XSelectInput";
    XSelectInput(display, DefaultRootWindow(display), SubstructureNotifyMask);

    /* We assume that the window manager provides the WM_STATE property on top-level
     * windows, as required by ICCCM 2.0.
     * If the window manager has not yet completed its initialisation, the WM_STATE atom
     * might not exist, in which case we create it. */

#ifdef XA_WM_STATE    /* probably in X11R7 */
    xa_wm_state = XA_WM_STATE;
#else
    xa_wm_state = XInternAtom(display, "WM_STATE", False);
#endif

    QTime elapsedTime;
    elapsedTime.start();
    while (mappingsCount) {
        if (elapsedTime.elapsed() > timeOutMS) {
            *errorMessage = QString::fromLatin1("X11: Timed out waiting for toplevel %1ms").arg(timeOutMS);
            return 0;
        }
        currentX11Function = "XNextEvent";
        unsigned errorCount = x11ErrorCount;
        XNextEvent(display, &event);
        if (x11ErrorCount > errorCount) {
            *errorMessage = QString::fromLatin1("X11: Error in XNextEvent");
            return 0;
        }
        switch (event.type) {
        case CreateNotify:
            // Window created, listen for its mapping now
            if (!event.xcreatewindow.send_event && !event.xcreatewindow.override_redirect)
                XSelectInput(display, event.xcreatewindow.window, PropertyChangeMask);
            break;
        case PropertyNotify:
            // Watch for map
            if (!event.xproperty.send_event && event.xproperty.atom == xa_wm_state) {
                bool mapped;                
                if (isMapped(display, xa_wm_state, event.xproperty.window, &mapped)) {
                    if (mapped && --mappingsCount == 0)
                        return event.xproperty.window;                    
                    // Past splash screen, listen for next window to be created
                    XSelectInput(display, DefaultRootWindow(display), SubstructureNotifyMask);
                } else {
                    // Some temporary window disappeared. Listen for next creation
                    XSelectInput(display, DefaultRootWindow(display), SubstructureNotifyMask);
                }
                // Main app window opened?
            }
            break;            
        default:
            break;
        }
    }
    *errorMessage = QString::fromLatin1("X11: Timed out waiting for toplevel %1ms").arg(timeOutMS);
    return 0;
}


class X11_WindowManager : public WindowManager
{
public:
    X11_WindowManager();
    ~X11_WindowManager();

protected:
    virtual bool isDisplayOpenImpl() const;
    virtual bool openDisplayImpl(QString *errorMessage);
    virtual QString waitForTopLevelWindowImpl(unsigned count, Q_PID, int timeOutMS, QString *errorMessage);
    virtual bool sendCloseEventImpl(const QString &winId, Q_PID pid, QString *errorMessage);

private:
    Display *m_display;
    const QByteArray m_displayVariable;
    XErrorHandler m_oldErrorHandler;
};

X11_WindowManager::X11_WindowManager() :
    m_display(0),
    m_displayVariable(qgetenv("DISPLAY")),
    m_oldErrorHandler(0)
{
}

X11_WindowManager::~X11_WindowManager()
{
    if (m_display) {
        XSetErrorHandler(m_oldErrorHandler);
        XCloseDisplay(m_display);
    }
}

bool X11_WindowManager::isDisplayOpenImpl() const
{
    return m_display != 0;
}

bool X11_WindowManager::openDisplayImpl(QString *errorMessage)
{
    if (m_displayVariable.isEmpty()) {
        *errorMessage = QLatin1String("X11: Display not set");
        return false;
    }    
    m_display = XOpenDisplay(NULL);
    if (!m_display) {
        *errorMessage = QString::fromLatin1("X11: Cannot open display %1.").arg(QString::fromLocal8Bit(m_displayVariable));
        return false;
    }

    m_oldErrorHandler = XSetErrorHandler(xErrorHandler);
    return true;
}

QString X11_WindowManager::waitForTopLevelWindowImpl(unsigned count, Q_PID, int timeOutMS, QString *errorMessage)
{
    const Window w = waitForTopLevelMapped(m_display, count, timeOutMS, errorMessage);
    if (w == 0)
        return QString();
    return QLatin1String("0x") + QString::number(w, 16);
}

 bool X11_WindowManager::sendCloseEventImpl(const QString &winId, Q_PID, QString *errorMessage)
 {
     // Get win id
     bool ok;
     const Window window = winId.toULong(&ok, 16);
     if (!ok) {
         *errorMessage = QString::fromLatin1("Invalid win id %1.").arg(winId);
         return false;
     }
     // Send a window manager close event
     XEvent ev;
     memset(&ev, 0, sizeof (ev));
     ev.xclient.type = ClientMessage;
     ev.xclient.window = window;
     ev.xclient.message_type = XInternAtom(m_display, "WM_PROTOCOLS", true);
     ev.xclient.format = 32;
     ev.xclient.data.l[0] = XInternAtom(m_display, "WM_DELETE_WINDOW", false);
     ev.xclient.data.l[1] = CurrentTime;
     // Window disappeared or some error triggered?
     unsigned errorCount = x11ErrorCount;
     currentX11Function = "XSendEvent";
     XSendEvent(m_display, window, False, NoEventMask, &ev);
     if (x11ErrorCount > errorCount) {
         *errorMessage = QString::fromLatin1("Error sending event to win id %1.").arg(winId);
         return false;
     }
     currentX11Function = "XSync";
     errorCount = x11ErrorCount;
     XSync(m_display, False);
     if (x11ErrorCount > errorCount) {
         *errorMessage = QString::fromLatin1("Error sending event to win id %1 (XSync).").arg(winId);
         return false;
     }
     return true;
 }

#endif

#if defined(Q_OS_WIN) && !defined(Q_OS_WINCE)
// Windows

 QString winErrorMessage(unsigned long error)
{
    QString rc = QString::fromLatin1("#%1: ").arg(error);
    ushort *lpMsgBuf;

    const int len = FormatMessage(
            FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
            NULL, error, 0, (LPTSTR)&lpMsgBuf, 0, NULL);
    if (len) {
        rc = QString::fromUtf16(lpMsgBuf, len);
        LocalFree(lpMsgBuf);
    } else {
        rc += QString::fromLatin1("<unknown error>");
    }
    return rc;
}

 class Win_WindowManager : public WindowManager
 {
 public:
     Win_WindowManager() {}

 protected:
     virtual bool isDisplayOpenImpl() const;
     virtual bool openDisplayImpl(QString *errorMessage);
     virtual QString waitForTopLevelWindowImpl(unsigned count, Q_PID, int timeOutMS, QString *errorMessage);
     virtual bool sendCloseEventImpl(const QString &winId, Q_PID pid, QString *errorMessage);

 private:
 };

bool Win_WindowManager::isDisplayOpenImpl() const
{
    return true;
}

bool Win_WindowManager::openDisplayImpl(QString *)
{
    return true;
}

// Enumerate window looking for toplevel of process id
struct FindProcessWindowEnumContext {
    FindProcessWindowEnumContext(DWORD pid) : window(0),processId(pid) {}

    HWND window;
    DWORD processId;
};

/* Check for the active main window of the Application
 * of class QWidget. */
static inline bool isQtMainWindow(HWND hwnd)
{
    static char buffer[MAX_PATH];
    if (!GetClassNameA(hwnd, buffer, MAX_PATH) || qstrcmp(buffer, "QWidget"))
        return false;
    WINDOWINFO windowInfo;
    if (!GetWindowInfo(hwnd, &windowInfo))
        return false;
    if (!(windowInfo.dwWindowStatus & WS_ACTIVECAPTION))
        return false;
    // Check the style for a real mainwindow
    const DWORD excluded = WS_DISABLED | WS_POPUP;
    const DWORD required = WS_CAPTION | WS_SYSMENU | WS_VISIBLE;    
    return (windowInfo.dwStyle & excluded) == 0
            && (windowInfo.dwStyle & required) == required;
}

static BOOL CALLBACK findProcessWindowEnumWindowProc(HWND hwnd, LPARAM lParam)
{
    DWORD processId = 0;
    FindProcessWindowEnumContext *context= reinterpret_cast<FindProcessWindowEnumContext *>(lParam);
    GetWindowThreadProcessId(hwnd, &processId);
    if (context->processId == processId && isQtMainWindow(hwnd)) {
        context->window = hwnd;
        return FALSE;
    }
    return TRUE;
}

QString Win_WindowManager::waitForTopLevelWindowImpl(unsigned /* count */, Q_PID pid, int timeOutMS, QString *errorMessage)
{    
    QTime elapsed;
    elapsed.start();
    // First, wait until the application is up
    if (WaitForInputIdle(pid->hProcess, timeOutMS) != 0) {
        *errorMessage = QString::fromLatin1("WaitForInputIdle time out after %1ms").arg(timeOutMS);
        return QString();
    }
    // Try to locate top level app window. App still might be in splash screen or initialization
    // phase.
    const int remainingMilliSeconds = qMax(timeOutMS - elapsed.elapsed(), 500);
    const int attempts = 10;
    const int intervalMilliSeconds = remainingMilliSeconds / attempts;
    for (int a = 0; a < attempts; a++) {
        FindProcessWindowEnumContext context(pid->dwProcessId);
        EnumWindows(findProcessWindowEnumWindowProc, reinterpret_cast<LPARAM>(&context));
        if (context.window)
            return QLatin1String("0x") + QString::number(reinterpret_cast<quintptr>(context.window), 16);
        sleepMS(intervalMilliSeconds);
    }
    *errorMessage = QString::fromLatin1("Unable to find toplevel of process %1 after %2ms.").arg(pid->dwProcessId).arg(timeOutMS);
    return QString();
}

bool Win_WindowManager::sendCloseEventImpl(const QString &winId, Q_PID, QString *errorMessage)
{   
    // Convert window back.
    quintptr winIdIntPtr;
    QTextStream str(const_cast<QString*>(&winId), QIODevice::ReadOnly);
    str.setIntegerBase(16);
    str >> winIdIntPtr;
    if (str.status() != QTextStream::Ok) {
        *errorMessage = QString::fromLatin1("Invalid win id %1.").arg(winId);
        return false;
    }
    if (!PostMessage(reinterpret_cast<HWND>(winIdIntPtr), WM_CLOSE, 0, 0)) {
        *errorMessage = QString::fromLatin1("Cannot send event to 0x%1: %2").arg(winIdIntPtr, 0, 16).arg(winErrorMessage(GetLastError()));
        return false;
    }
    return true;
}
#endif

// ------- Default implementation

WindowManager::WindowManager()
{
}

WindowManager::~WindowManager()
{
}

QSharedPointer<WindowManager> WindowManager::create()
{
#ifdef Q_WS_X11
    return QSharedPointer<WindowManager>(new X11_WindowManager);
#endif
#if defined(Q_OS_WIN) && !defined(Q_OS_WINCE)
    return QSharedPointer<WindowManager>(new Win_WindowManager);
#else
    return QSharedPointer<WindowManager>(new WindowManager);
#endif
}

static inline QString msgNoDisplayOpen() { return QLatin1String("No display opened."); }

bool WindowManager::openDisplay(QString *errorMessage)
{
    if (isDisplayOpen())
        return true;
    return openDisplayImpl(errorMessage);
}

bool WindowManager::isDisplayOpen() const
{
    return isDisplayOpenImpl();
}



QString WindowManager::waitForTopLevelWindow(unsigned count, Q_PID pid, int timeOutMS, QString *errorMessage)
{
    if (!isDisplayOpen()) {
        *errorMessage = msgNoDisplayOpen();
        return QString();
    }
    return waitForTopLevelWindowImpl(count, pid, timeOutMS, errorMessage);
}

bool WindowManager::sendCloseEvent(const QString &winId, Q_PID pid, QString *errorMessage)
{
    if (!isDisplayOpen()) {
        *errorMessage = msgNoDisplayOpen();
        return false;
    }
    return sendCloseEventImpl(winId, pid, errorMessage);
}

// Default Implementation
bool WindowManager::openDisplayImpl(QString *errorMessage)
{
    *errorMessage = QLatin1String("Not implemented.");
    return false;
}

bool WindowManager::isDisplayOpenImpl() const
{
    return false;
}

QString WindowManager::waitForTopLevelWindowImpl(unsigned, Q_PID, int, QString *errorMessage)
{
    *errorMessage = QLatin1String("Not implemented.");
    return QString();
}

bool WindowManager::sendCloseEventImpl(const QString &, Q_PID, QString *errorMessage)
{
    *errorMessage = QLatin1String("Not implemented.");
    return false;
}

void WindowManager::sleepMS(int milliSeconds)
{
    FriendlySleepyThread::sleepMS(milliSeconds);
}
