/*
 * synergy -- mouse and keyboard sharing utility
 * Copyright (C) 2012-2016 Symless Ltd.
 * Copyright (C) 2002 Chris Schoeneman
 * 
 * This package is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * found in the file LICENSE that should have accompanied this file.
 * 
 * This package 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 "platform/XWindowsScreenSaver.h"

#include "platform/XWindowsUtil.h"
#include "synergy/IPlatformScreen.h"
#include "base/Log.h"
#include "base/Event.h"
#include "base/IEventQueue.h"
#include "base/TMethodEventJob.h"

#include <X11/Xatom.h>
#if HAVE_X11_EXTENSIONS_XTEST_H
#    include <X11/extensions/XTest.h>
#else
#    error The XTest extension is required to build synergy
#endif
#if HAVE_X11_EXTENSIONS_DPMS_H
extern "C" {
#    include <X11/Xmd.h>
#    include <X11/extensions/dpms.h>
#    if !HAVE_DPMS_PROTOTYPES
#        undef DPMSModeOn
#        undef DPMSModeStandby
#        undef DPMSModeSuspend
#        undef DPMSModeOff
#        define DPMSModeOn        0
#        define DPMSModeStandby    1
#        define DPMSModeSuspend    2
#        define DPMSModeOff        3
extern Bool DPMSQueryExtension(Display *, int *, int *);
extern Bool DPMSCapable(Display *);
extern Status DPMSEnable(Display *);
extern Status DPMSDisable(Display *);
extern Status DPMSForceLevel(Display *, CARD16);
extern Status DPMSInfo(Display *, CARD16 *, BOOL *);
#    endif
}
#endif

//
// XWindowsScreenSaver
//

XWindowsScreenSaver::XWindowsScreenSaver(
                Display* display, Window window, void* eventTarget, IEventQueue* events) :
    m_display(display),
    m_xscreensaverSink(window),
    m_eventTarget(eventTarget),
    m_xscreensaver(None),
    m_xscreensaverActive(false),
    m_dpms(false),
    m_disabled(false),
    m_suppressDisable(false),
    m_disableTimer(NULL),
    m_disablePos(0),
    m_events(events)
{
    // get atoms
    m_atomScreenSaver           = XInternAtom(m_display,
                                        "SCREENSAVER", False);
    m_atomScreenSaverVersion    = XInternAtom(m_display,
                                        "_SCREENSAVER_VERSION", False);
    m_atomScreenSaverActivate   = XInternAtom(m_display,
                                        "ACTIVATE", False);
    m_atomScreenSaverDeactivate = XInternAtom(m_display,
                                        "DEACTIVATE", False);

    // check for DPMS extension.  this is an alternative screen saver
    // that powers down the display.
#if HAVE_X11_EXTENSIONS_DPMS_H
    int eventBase, errorBase;
    if (DPMSQueryExtension(m_display, &eventBase, &errorBase)) {
        if (DPMSCapable(m_display)) {
            // we have DPMS
            m_dpms  = true;
        }
    }
#endif

    // watch top-level windows for changes
    bool error = false;
    {
        XWindowsUtil::ErrorLock lock(m_display, &error);
        Window root = DefaultRootWindow(m_display);
        XWindowAttributes attr;
        XGetWindowAttributes(m_display, root, &attr);
        m_rootEventMask = attr.your_event_mask;
        XSelectInput(m_display, root, m_rootEventMask | SubstructureNotifyMask);
    }
    if (error) {
        LOG((CLOG_DEBUG "didn't set root event mask"));
        m_rootEventMask = 0;
    }

    // get the built-in settings
    XGetScreenSaver(m_display, &m_timeout, &m_interval,
                                &m_preferBlanking, &m_allowExposures);

    // get the DPMS settings
    m_dpmsEnabled = isDPMSEnabled();

    // get the xscreensaver window, if any
    if (!findXScreenSaver()) {
        setXScreenSaver(None);
    }

    // install disable timer event handler
    m_events->adoptHandler(Event::kTimer, this,
                            new TMethodEventJob<XWindowsScreenSaver>(this,
                                &XWindowsScreenSaver::handleDisableTimer));
}

XWindowsScreenSaver::~XWindowsScreenSaver()
{
    // done with disable job
    if (m_disableTimer != NULL) {
        m_events->deleteTimer(m_disableTimer);
    }
    m_events->removeHandler(Event::kTimer, this);

    if (m_display != NULL) {
        enableDPMS(m_dpmsEnabled);
        XSetScreenSaver(m_display, m_timeout, m_interval,
                                m_preferBlanking, m_allowExposures);
        clearWatchForXScreenSaver();
        XWindowsUtil::ErrorLock lock(m_display);
        XSelectInput(m_display, DefaultRootWindow(m_display), m_rootEventMask);
    }
}

void
XWindowsScreenSaver::destroy()
{
    m_display = NULL;
    delete this;
}

bool
XWindowsScreenSaver::handleXEvent(const XEvent* xevent)
{
    switch (xevent->type) {
    case CreateNotify:
        if (m_xscreensaver == None) {
            if (isXScreenSaver(xevent->xcreatewindow.window)) {
                // found the xscreensaver
                setXScreenSaver(xevent->xcreatewindow.window);
            }
            else {
                // another window to watch.  to detect the xscreensaver
                // window we look for a property but that property may
                // not yet exist by the time we get this event so we
                // have to watch the window for property changes.
                // this would be so much easier if xscreensaver did the
                // smart thing and stored its window in a property on
                // the root window.
                addWatchXScreenSaver(xevent->xcreatewindow.window);
            }
        }
        break;

    case DestroyNotify:
        if (xevent->xdestroywindow.window == m_xscreensaver) {
            // xscreensaver is gone
            LOG((CLOG_DEBUG "xscreensaver died"));
            setXScreenSaver(None);
            return true;
        }
        break;

    case PropertyNotify:
        if (xevent->xproperty.state == PropertyNewValue) {
            if (isXScreenSaver(xevent->xproperty.window)) {
                // found the xscreensaver
                setXScreenSaver(xevent->xcreatewindow.window);
            }
        }
        break;

    case MapNotify:
        if (xevent->xmap.window == m_xscreensaver) {
            // xscreensaver has activated
            setXScreenSaverActive(true);
            return true;
        }
        break;

    case UnmapNotify:
        if (xevent->xunmap.window == m_xscreensaver) {
            // xscreensaver has deactivated
            setXScreenSaverActive(false);
            return true;
        }
        break;
    }

    return false;
}

void
XWindowsScreenSaver::enable()
{
    // for xscreensaver
    m_disabled = false;
    updateDisableTimer();

    // for built-in X screen saver
    XSetScreenSaver(m_display, m_timeout, m_interval,
                                m_preferBlanking, m_allowExposures);

    // for DPMS
    enableDPMS(m_dpmsEnabled);
}

void
XWindowsScreenSaver::disable()
{
    // for xscreensaver
    m_disabled = true;
    updateDisableTimer();

    // use built-in X screen saver
    XGetScreenSaver(m_display, &m_timeout, &m_interval,
                                &m_preferBlanking, &m_allowExposures);
    XSetScreenSaver(m_display, 0, m_interval,
                                m_preferBlanking, m_allowExposures);

    // for DPMS
    m_dpmsEnabled = isDPMSEnabled();
    enableDPMS(false);

    // FIXME -- now deactivate?
}

void
XWindowsScreenSaver::activate()
{
    // remove disable job timer
    m_suppressDisable = true;
    updateDisableTimer();

    // enable DPMS if it was enabled
    enableDPMS(m_dpmsEnabled);

    // try xscreensaver
    findXScreenSaver();
    if (m_xscreensaver != None) {
        sendXScreenSaverCommand(m_atomScreenSaverActivate);
        return;
    }

    // try built-in X screen saver
    if (m_timeout != 0) {
        XForceScreenSaver(m_display, ScreenSaverActive);
    }

    // try DPMS
    activateDPMS(true);
}

void
XWindowsScreenSaver::deactivate()
{
    // reinstall disable job timer
    m_suppressDisable = false;
    updateDisableTimer();

    // try DPMS
    activateDPMS(false);

    // disable DPMS if screen saver is disabled
    if (m_disabled) {
        enableDPMS(false);
    }

    // try xscreensaver
    findXScreenSaver();
    if (m_xscreensaver != None) {
        sendXScreenSaverCommand(m_atomScreenSaverDeactivate);
        return;
    }

    // use built-in X screen saver
    XForceScreenSaver(m_display, ScreenSaverReset);
}

bool
XWindowsScreenSaver::isActive() const
{
    // check xscreensaver
    if (m_xscreensaver != None) {
        return m_xscreensaverActive;
    }

    // check DPMS
    if (isDPMSActivated()) {
        return true;
    }

    // can't check built-in X screen saver activity
    return false;
}

bool
XWindowsScreenSaver::findXScreenSaver()
{
    // do nothing if we've already got the xscreensaver window
    if (m_xscreensaver == None) {
        // find top-level window xscreensaver window
        Window root = DefaultRootWindow(m_display);
        Window rw, pw, *cw;
        unsigned int nc;
        if (XQueryTree(m_display, root, &rw, &pw, &cw, &nc)) {
            for (unsigned int i = 0; i < nc; ++i) {
                if (isXScreenSaver(cw[i])) {
                    setXScreenSaver(cw[i]);
                    break;
                }
            }
            XFree(cw);
        }
    }

    return (m_xscreensaver != None);
}

void
XWindowsScreenSaver::setXScreenSaver(Window window)
{
    LOG((CLOG_DEBUG "xscreensaver window: 0x%08x", window));

    // save window
    m_xscreensaver = window;

    if (m_xscreensaver != None) {
        // clear old watch list
        clearWatchForXScreenSaver();

        // see if xscreensaver is active
        bool error = false;
        XWindowAttributes attr;
        {
            XWindowsUtil::ErrorLock lock(m_display, &error);
            XGetWindowAttributes(m_display, m_xscreensaver, &attr);
        }
        setXScreenSaverActive(!error && attr.map_state != IsUnmapped);

        // save current DPMS state;  xscreensaver may have changed it.
        m_dpmsEnabled = isDPMSEnabled();
    }
    else {
        // screen saver can't be active if it doesn't exist
        setXScreenSaverActive(false);

        // start watching for xscreensaver
        watchForXScreenSaver();
    }
}

bool
XWindowsScreenSaver::isXScreenSaver(Window w) const
{
    // check for m_atomScreenSaverVersion string property
    Atom type;
    return (XWindowsUtil::getWindowProperty(m_display, w,
                                    m_atomScreenSaverVersion,
                                    NULL, &type, NULL, False) &&
                                type == XA_STRING);
}

void
XWindowsScreenSaver::setXScreenSaverActive(bool activated)
{
    if (m_xscreensaverActive != activated) {
        LOG((CLOG_DEBUG "xscreensaver %s on window 0x%08x", activated ? "activated" : "deactivated", m_xscreensaver));
        m_xscreensaverActive = activated;

        // if screen saver was activated forcefully (i.e. against
        // our will) then just accept it.  don't try to keep it
        // from activating since that'll just pop up the password
        // dialog if locking is enabled.
        m_suppressDisable = activated;
        updateDisableTimer();

        if (activated) {
            m_events->addEvent(Event(
                            m_events->forIPrimaryScreen().screensaverActivated(),
                            m_eventTarget));
        }
        else {
            m_events->addEvent(Event(
                            m_events->forIPrimaryScreen().screensaverDeactivated(),
                            m_eventTarget));
        }
    }
}

void
XWindowsScreenSaver::sendXScreenSaverCommand(Atom cmd, long arg1, long arg2)
{
    XEvent event;
    event.xclient.type         = ClientMessage;
    event.xclient.display      = m_display;
    event.xclient.window       = m_xscreensaverSink;
    event.xclient.message_type = m_atomScreenSaver;
    event.xclient.format       = 32;
    event.xclient.data.l[0]    = static_cast<long>(cmd);
    event.xclient.data.l[1]    = arg1;
    event.xclient.data.l[2]    = arg2;
    event.xclient.data.l[3]    = 0;
    event.xclient.data.l[4]    = 0;

    LOG((CLOG_DEBUG "send xscreensaver command: %d %d %d", (long)cmd, arg1, arg2));
    bool error = false;
    {
        XWindowsUtil::ErrorLock lock(m_display, &error);
        XSendEvent(m_display, m_xscreensaver, False, 0, &event);
    }
    if (error) {
        findXScreenSaver();
    }
}

void
XWindowsScreenSaver::watchForXScreenSaver()
{
    // clear old watch list
    clearWatchForXScreenSaver();

    // add every child of the root to the list of windows to watch
    Window root = DefaultRootWindow(m_display);
    Window rw, pw, *cw;
    unsigned int nc;
    if (XQueryTree(m_display, root, &rw, &pw, &cw, &nc)) {
        for (unsigned int i = 0; i < nc; ++i) {
            addWatchXScreenSaver(cw[i]);
        }
        XFree(cw);
    }

    // now check for xscreensaver window in case it set the property
    // before we could request property change events.
    if (findXScreenSaver()) {
        // found it so clear out our watch list
        clearWatchForXScreenSaver();
    }
}

void
XWindowsScreenSaver::clearWatchForXScreenSaver()
{
    // stop watching all windows
    XWindowsUtil::ErrorLock lock(m_display);
    for (WatchList::iterator index = m_watchWindows.begin();
                                index != m_watchWindows.end(); ++index) {
        XSelectInput(m_display, index->first, index->second);
    }
    m_watchWindows.clear();
}

void
XWindowsScreenSaver::addWatchXScreenSaver(Window window)
{
    // get window attributes
    bool error = false;
    XWindowAttributes attr;
    {
        XWindowsUtil::ErrorLock lock(m_display, &error);
        XGetWindowAttributes(m_display, window, &attr);
    }

    // if successful and window uses override_redirect (like xscreensaver
    // does) then watch it for property changes.  
    if (!error && attr.override_redirect == True) {
        error = false;
        {
            XWindowsUtil::ErrorLock lock(m_display, &error);
            XSelectInput(m_display, window,
                                attr.your_event_mask | PropertyChangeMask);
        }
        if (!error) {
            // if successful then add the window to our list
            m_watchWindows.insert(std::make_pair(window, attr.your_event_mask));
        }
    }
}

void
XWindowsScreenSaver::updateDisableTimer()
{
    if (m_disabled && !m_suppressDisable && m_disableTimer == NULL) {
        // 5 seconds should be plenty often to suppress the screen saver
        m_disableTimer = m_events->newTimer(5.0, this);
    }
    else if ((!m_disabled || m_suppressDisable) && m_disableTimer != NULL) {
        m_events->deleteTimer(m_disableTimer);
        m_disableTimer = NULL;
    }
}

void
XWindowsScreenSaver::handleDisableTimer(const Event&, void*)
{
    // send fake mouse motion directly to xscreensaver
    if (m_xscreensaver != None) {
        XEvent event;
        event.xmotion.type         = MotionNotify;
        event.xmotion.display      = m_display;
        event.xmotion.window       = m_xscreensaver;
        event.xmotion.root         = DefaultRootWindow(m_display);
        event.xmotion.subwindow    = None;
        event.xmotion.time         = CurrentTime;
        event.xmotion.x            = m_disablePos;
        event.xmotion.y            = 0;
        event.xmotion.x_root       = m_disablePos;
        event.xmotion.y_root       = 0;
        event.xmotion.state        = 0;
        event.xmotion.is_hint      = NotifyNormal;
        event.xmotion.same_screen  = True;

        XWindowsUtil::ErrorLock lock(m_display);
        XSendEvent(m_display, m_xscreensaver, False, 0, &event);

        m_disablePos = 20 - m_disablePos;
    }
}

void
XWindowsScreenSaver::activateDPMS(bool activate)
{
#if HAVE_X11_EXTENSIONS_DPMS_H
    if (m_dpms) {
        // DPMSForceLevel will generate a BadMatch if DPMS is disabled
        XWindowsUtil::ErrorLock lock(m_display);
        DPMSForceLevel(m_display, activate ? DPMSModeStandby : DPMSModeOn);
    }
#endif
}

void
XWindowsScreenSaver::enableDPMS(bool enable)
{
#if HAVE_X11_EXTENSIONS_DPMS_H
    if (m_dpms) {
        if (enable) {
            DPMSEnable(m_display);
        }
        else {
            DPMSDisable(m_display);
        }
    }
#endif
}

bool
XWindowsScreenSaver::isDPMSEnabled() const
{
#if HAVE_X11_EXTENSIONS_DPMS_H
    if (m_dpms) {
        CARD16 level;
        BOOL state;
        DPMSInfo(m_display, &level, &state);
        return (state != False);
    }
    else {
        return false;
    }
#else
    return false;
#endif
}

bool
XWindowsScreenSaver::isDPMSActivated() const
{
#if HAVE_X11_EXTENSIONS_DPMS_H
    if (m_dpms) {
        CARD16 level;
        BOOL state;
        DPMSInfo(m_display, &level, &state);
        return (level != DPMSModeOn);
    }
    else {
        return false;
    }
#else
    return false;
#endif
}
