/********************************************************************
 KSld - the KDE Screenlocker Daemon
 This file is part of the KDE project.

Copyright (C) 1999 Martin R. Jones <mjones@kde.org>
Copyright (C) 2002 Luboš Luňák <l.lunak@kde.org>
Copyright (C) 2003 Oswald Buddenhagen <ossi@kde.org>
Copyright (C) 2008 Chani Armitage <chanika@gmail.com>
Copyright (C) 2011 Martin Gräßlin <mgraesslin@kde.org>

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 2 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 "lockwindow.h"
#include "autologout.h"
#include "ksldapp.h"
// KDE
#include <KDE/KApplication>
#include <KDE/KDebug>
#include <KDE/KXErrorHandler>
// Qt
#include <QtCore/QTimer>
#include <QtCore/QPointer>
#include <QtGui/QDesktopWidget>
#include <QtGui/QPainter>
#include <QtGui/QX11Info>
// X11
#include <X11/Xatom.h>
#include <fixx11h.h>

static Window gVRoot = 0;
static Window gVRootData = 0;
static Atom   gXA_VROOT;
static Atom   gXA_SCREENSAVER_VERSION;

//#define CHECK_XSELECTINPUT
#ifdef CHECK_XSELECTINPUT
#include <dlfcn.h>
static bool check_xselectinput = false;
extern "C"
int XSelectInput( Display* dpy, Window w, long e )
{
    typedef int (*ptr)(Display*, Window, long);
    static ptr fun = NULL;
    if( fun == NULL )
        fun = (ptr)dlsym( RTLD_NEXT, "XSelectInput" );
    if( check_xselectinput && w == DefaultRootWindow( dpy ))
        kDebug() << kBacktrace();
    return fun( dpy, w, e );
}
#endif

namespace ScreenLocker
{

LockWindow::LockWindow()
    : QWidget()
    , m_autoLogoutTimer(new QTimer(this))
{
    initialize();
}

LockWindow::~LockWindow()
{
}

void LockWindow::initialize()
{
    kapp->installX11EventFilter(this);

    XWindowAttributes rootAttr;
    QX11Info info;
    XGetWindowAttributes(QX11Info::display(), RootWindow(QX11Info::display(),
                                                         info.screen()), &rootAttr);
    QApplication::desktop(); // make Qt set its event mask on the root window first
#ifdef CHECK_XSELECTINPUT
    check_xselectinput = true;
#endif
    XSelectInput( QX11Info::display(), QX11Info::appRootWindow(),
                  SubstructureNotifyMask | rootAttr.your_event_mask );
    // Get root window size
    updateGeometry();

    // virtual root property
    gXA_VROOT = XInternAtom (QX11Info::display(), "__SWM_VROOT", False);
    gXA_SCREENSAVER_VERSION = XInternAtom (QX11Info::display(), "_SCREENSAVER_VERSION", False);

        // read the initial information about all toplevel windows
    Window r, p;
    Window* real;
    unsigned nreal;
    if( XQueryTree( x11Info().display(), x11Info().appRootWindow(), &r, &p, &real, &nreal )
        && real != NULL ) {
        KXErrorHandler err; // ignore X errors here
        for( unsigned i = 0; i < nreal; ++i ) {
            XWindowAttributes winAttr;
            if (XGetWindowAttributes(QX11Info::display(), real[ i ], &winAttr)) {
                WindowInfo info;
                info.window = real[ i ];
                info.viewable = ( winAttr.map_state == IsViewable );
                m_windowInfo.append( info ); // ordered bottom to top
            }
        }
        XFree( real );
    }
    m_autoLogoutTimer->setSingleShot(true);
    connect(m_autoLogoutTimer, SIGNAL(timeout()), SLOT(autoLogoutTimeout()));
    connect(QApplication::desktop(), SIGNAL(resized(int)), SLOT(updateGeometry()));
    connect(QApplication::desktop(), SIGNAL(screenCountChanged(int)), SLOT(updateGeometry()));
}

void LockWindow::showLockWindow()
{
    Visual* visual = CopyFromParent;
    int depth = CopyFromParent;
    XSetWindowAttributes attrs;
    int flags = CWOverrideRedirect;
    attrs.override_redirect = 1;
    hide();
    Window w = XCreateWindow( x11Info().display(), RootWindow( x11Info().display(), x11Info().screen()),
        x(), y(), width(), height(), 0, depth, InputOutput, visual, flags, &attrs );

    create( w, false, true );

    // Some xscreensaver hacks check for this property
    const char *version = "KDE 4.0";

    XChangeProperty (QX11Info::display(), winId(),
                     gXA_SCREENSAVER_VERSION, XA_STRING, 8, PropModeReplace,
                     (unsigned char *) version, strlen(version));


    XSetWindowAttributes attr;
    attr.event_mask = KeyPressMask | ButtonPressMask | PointerMotionMask |
                        VisibilityChangeMask | ExposureMask;
    XChangeWindowAttributes(QX11Info::display(), winId(),
                            CWEventMask, &attr);

    QPalette p = palette();
    p.setColor(backgroundRole(), Qt::black);
    setPalette(p);
    setAttribute(Qt::WA_PaintOnScreen, true);
    setAttribute(Qt::WA_NoSystemBackground, false);

    kDebug() << "Lock window Id: " << winId();

    move(0, 0);
    XSync(QX11Info::display(), False);

    setVRoot( winId(), winId() );
    if (KSldApp::self()->autoLogoutTimeout()) {
        m_autoLogoutTimer->start(KSldApp::self()->autoLogoutTimeout());
    }
}

//---------------------------------------------------------------------------
//
// Hide the screen locker window
//
void LockWindow::hideLockWindow()
{
  if (m_autoLogoutTimer->isActive()) {
      m_autoLogoutTimer->stop();
  }
  emit userActivity();
  hide();
  lower();
  removeVRoot(winId());
  XDeleteProperty(QX11Info::display(), winId(), gXA_SCREENSAVER_VERSION);
  if ( gVRoot ) {
      unsigned long vroot_data[1] = { gVRootData };
      XChangeProperty(QX11Info::display(), gVRoot, gXA_VROOT, XA_WINDOW, 32,
                      PropModeReplace, (unsigned char *)vroot_data, 1);
      gVRoot = 0;
  }
  XSync(QX11Info::display(), False);
}

//---------------------------------------------------------------------------
static int ignoreXError(Display *, XErrorEvent *)
{
    return 0;
}

//---------------------------------------------------------------------------
//
// Save the current virtual root window
//
void LockWindow::saveVRoot()
{
  Window rootReturn, parentReturn, *children;
  unsigned int numChildren;
  QX11Info info;
  Window root = RootWindowOfScreen(ScreenOfDisplay(QX11Info::display(), info.screen()));

  gVRoot = 0;
  gVRootData = 0;

  int (*oldHandler)(Display *, XErrorEvent *);
  oldHandler = XSetErrorHandler(ignoreXError);

  if (XQueryTree(QX11Info::display(), root, &rootReturn, &parentReturn,
      &children, &numChildren))
  {
    for (unsigned int i = 0; i < numChildren; i++)
    {
      Atom actual_type;
      int actual_format;
      unsigned long nitems, bytesafter;
      unsigned char *newRoot = 0;

      if ((XGetWindowProperty(QX11Info::display(), children[i], gXA_VROOT, 0, 1,
          False, XA_WINDOW, &actual_type, &actual_format, &nitems, &bytesafter,
          &newRoot) == Success) && newRoot)
      {
        gVRoot = children[i];
        Window *dummy = (Window*)newRoot;
        gVRootData = *dummy;
        XFree ((char*) newRoot);
        break;
      }
    }
    if (children)
    {
      XFree((char *)children);
    }
  }

  XSetErrorHandler(oldHandler);
}

//---------------------------------------------------------------------------
//
// Set the virtual root property
//
void LockWindow::setVRoot(Window win, Window vr)
{
    if (gVRoot)
        removeVRoot(gVRoot);

        QX11Info info;
    unsigned long rw = RootWindowOfScreen(ScreenOfDisplay(QX11Info::display(), info.screen()));
    unsigned long vroot_data[1] = { vr };

    Window rootReturn, parentReturn, *children;
    unsigned int numChildren;
    Window top = win;
    while (1) {
        if (!XQueryTree(QX11Info::display(), top , &rootReturn, &parentReturn,
                                 &children, &numChildren))
            return;
        if (children)
            XFree((char *)children);
        if (parentReturn == rw) {
            break;
        } else
            top = parentReturn;
    }

    XChangeProperty(QX11Info::display(), top, gXA_VROOT, XA_WINDOW, 32,
                     PropModeReplace, (unsigned char *)vroot_data, 1);
}

//---------------------------------------------------------------------------
//
// Remove the virtual root property
//
void LockWindow::removeVRoot(Window win)
{
    XDeleteProperty (QX11Info::display(), win, gXA_VROOT);
}

static void fakeFocusIn( WId window )
{
    // We have keyboard grab, so this application will
    // get keyboard events even without having focus.
    // Fake FocusIn to make Qt realize it has the active
    // window, so that it will correctly show cursor in the dialog.
    XEvent ev;
    memset(&ev, 0, sizeof(ev));
    ev.xfocus.display = QX11Info::display();
    ev.xfocus.type = FocusIn;
    ev.xfocus.window = window;
    ev.xfocus.mode = NotifyNormal;
    ev.xfocus.detail = NotifyAncestor;
    XSendEvent( QX11Info::display(), window, False, NoEventMask, &ev );
}

// Event filter
bool LockWindow::x11Event(XEvent* event)
{
    bool ret = false;
    switch (event->type) {
        case ButtonPress:
        case ButtonRelease:
        case KeyPress:
        case KeyRelease:
        case MotionNotify:
            if (KSldApp::self()->isGraceTime()) {
                KSldApp::self()->unlock();
                return true;
            }
            if (m_autoLogoutTimer->isActive()) {
                m_autoLogoutTimer->start(KSldApp::self()->autoLogoutTimeout());
            }
            emit userActivity();
            if (!m_lockWindows.isEmpty()) {
                XEvent ev2 = *event;
                Window root_return;
                int x_return, y_return;
                unsigned int width_return, height_return, border_width_return, depth_return;
                WId targetWindow = 0;
                KXErrorHandler err; // ignore X errors
                foreach (WId window, m_lockWindows) {
                    if (XGetGeometry(QX11Info::display(), window, &root_return,
                                &x_return, &y_return,
                                &width_return, &height_return,
                                &border_width_return, &depth_return)
                        &&
                        (event->xkey.x>=x_return && event->xkey.x<=x_return+(int)width_return)
                        &&
                        (event->xkey.y>=y_return && event->xkey.y<=y_return+(int)height_return) ) {
                        targetWindow = window;
                        ev2.xkey.window = ev2.xkey.subwindow = targetWindow;
                        ev2.xkey.x = event->xkey.x - x_return;
                        ev2.xkey.y = event->xkey.y - y_return;
                        break;
                    }
                }
                XSendEvent(QX11Info::display(), targetWindow, False, NoEventMask, &ev2);
                ret = true;
            }
            break;
        case ConfigureNotify: // from SubstructureNotifyMask on the root window
            if(event->xconfigure.event == QX11Info::appRootWindow()) {
                int index = findWindowInfo( event->xconfigure.window );
                if( index >= 0 ) {
                    int index2 = event->xconfigure.above ? findWindowInfo( event->xconfigure.above ) : 0;
                    if( index2 < 0 )
                        kDebug(1204) << "Unknown above for ConfigureNotify";
                    else { // move just above the other window
                        if( index2 < index )
                            ++index2;
                        m_windowInfo.move( index, index2 );
                    }
                } else
                    kDebug(1204) << "Unknown toplevel for ConfigureNotify";
                //kDebug() << "ConfigureNotify:";
                //the stacking order changed, so let's change the stacking order again to what we want
                stayOnTop();
            }
            break;
        case MapNotify: // from SubstructureNotifyMask on the root window
            if( event->xmap.event == QX11Info::appRootWindow()) {
                kDebug(1204) << "MapNotify:" << event->xmap.window;
                int index = findWindowInfo( event->xmap.window );
                if( index >= 0 )
                    m_windowInfo[ index ].viewable = true;
                else
                    kDebug(1204) << "Unknown toplevel for MapNotify";
                KXErrorHandler err; // ignore X errors here
                if (isLockWindow(event->xmap.window)) {
                    if (m_lockWindows.contains(event->xmap.window)) {
                        kDebug() << "uhoh! duplicate!";
                    } else {
                        if (!isVisible()) {
                            // not yet shown and we have a lock window, so we show our own window
                            show();
                            setCursor(Qt::ArrowCursor);
                        }
                        m_lockWindows.prepend(event->xmap.window);
                        fakeFocusIn(event->xmap.window);
                    }
                }
                stayOnTop();
            }
            break;
        case UnmapNotify:
            if (event->xunmap.event == QX11Info::appRootWindow()) {
                kDebug(1204) << "UnmapNotify:" << event->xunmap.window;
                int index = findWindowInfo( event->xunmap.window );
                if( index >= 0 )
                    m_windowInfo[ index ].viewable = false;
                else
                    kDebug(1204) << "Unknown toplevel for MapNotify";
                m_lockWindows.removeAll(event->xunmap.window);
            }
            break;
        case CreateNotify:
            if (event->xcreatewindow.parent == QX11Info::appRootWindow()) {
                kDebug() << "CreateNotify:" << event->xcreatewindow.window;
                int index = findWindowInfo( event->xcreatewindow.window );
                if( index >= 0 )
                    kDebug() << "Already existing toplevel for CreateNotify";
                else {
                    WindowInfo info;
                    info.window = event->xcreatewindow.window;
                    info.viewable = false;
                    m_windowInfo.append( info );
                }
            }
            break;
        case DestroyNotify:
            if (event->xdestroywindow.event == QX11Info::appRootWindow()) {
                int index = findWindowInfo( event->xdestroywindow.window );
                if( index >= 0 )
                    m_windowInfo.removeAt( index );
                else
                    kDebug() << "Unknown toplevel for DestroyNotify";
            }
            break;
        case ReparentNotify:
            if (event->xreparent.event == QX11Info::appRootWindow() && event->xreparent.parent != QX11Info::appRootWindow()) {
                int index = findWindowInfo( event->xreparent.window );
                if( index >= 0 )
                    m_windowInfo.removeAt( index );
                else
                    kDebug() << "Unknown toplevel for ReparentNotify away";
            } else if (event->xreparent.parent == QX11Info::appRootWindow()) {
                int index = findWindowInfo( event->xreparent.window );
                if( index >= 0 )
                    kDebug() << "Already existing toplevel for ReparentNotify";
                else {
                    WindowInfo info;
                    info.window = event->xreparent.window;
                    info.viewable = false;
                    m_windowInfo.append( info );
                }
            }
            break;
        case CirculateNotify:
            if (event->xcirculate.event == QX11Info::appRootWindow()) {
                int index = findWindowInfo( event->xcirculate.window );
                if( index >= 0 ) {
                    m_windowInfo.move( index, event->xcirculate.place == PlaceOnTop ? m_windowInfo.size() - 1 : 0 );
                } else
                    kDebug() << "Unknown toplevel for CirculateNotify";
            }
            break;
    }
    return ret;
}

int LockWindow::findWindowInfo(Window w)
{
    for( int i = 0;
         i < m_windowInfo.size();
         ++i )
        if( m_windowInfo[ i ].window == w )
            return i;
    return -1;
}

void LockWindow::stayOnTop()
{

    // this restacking is written in a way so that
    // if the stacking positions actually don't change,
    // all restacking operations will be no-op,
    // and no ConfigureNotify will be generated,
    // thus avoiding possible infinite loops
    QVector< Window > stack( m_lockWindows.count() + 1 );
    int count = 0;
    foreach( WId w, m_lockWindows )
        stack[ count++ ] = w;
    // finally, the lock window
    stack[ count++ ] = winId();
    // do the actual restacking if needed
    XRaiseWindow( x11Info().display(), stack[ 0 ] );
    if( count > 1 )
        XRestackWindows( x11Info().display(), stack.data(), count );
}

bool LockWindow::isLockWindow(Window id)
{
    Atom tag = XInternAtom(QX11Info::display(), "_KDE_SCREEN_LOCKER", False);
    Atom actualType;
    int actualFormat;
    unsigned long nitems, remaining;
    unsigned char *data = 0;
    Display *display = QX11Info::display();

    int result = XGetWindowProperty(display, id, tag, 0, 1, False, tag, &actualType,
            &actualFormat, &nitems, &remaining, &data);

    bool lockWindow = false;
    if (result == Success && actualType == tag) {
        lockWindow = true;
    }
    if (data) {
        XFree(data);
    }
    return lockWindow;
}

void LockWindow::autoLogoutTimeout()
{
    QDesktopWidget *desktop = QApplication::desktop();
    QRect screenRect;
    if (desktop->screenCount() > 1) {
        screenRect = desktop->screenGeometry(desktop->screenNumber(QCursor::pos()));
    } else {
        screenRect = desktop->screenGeometry();
    }

    QPointer<AutoLogout> dlg = new AutoLogout(this);
    dlg->adjustSize();

    QRect rect = dlg->geometry();
    rect.moveCenter(screenRect.center());
    dlg->move(rect.topLeft());

    Atom tag = XInternAtom(QX11Info::display(), "_KDE_SCREEN_LOCKER", False);
    XChangeProperty(QX11Info::display(), dlg->winId(), tag, tag, 32, PropModeReplace, 0, 0);

    dlg->exec();
    delete dlg;

    // start the timer again - only if the window is still shown
    if (isVisible()) {
        m_autoLogoutTimer->start(KSldApp::self()->autoLogoutTimeout());
    }
}

void LockWindow::updateGeometry()
{
    QDesktopWidget *desktop = QApplication::desktop();
    setGeometry(desktop->geometry());
}

void LockWindow::paintEvent(QPaintEvent* )
{
    QPainter p(this);
    p.setBrush(QBrush(Qt::black));
    p.drawRect(geometry());
}

}
