/*
 * synergy -- mouse and keyboard sharing utility
 * 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 COPYING 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.
 */

#include "CMSWindowsScreenSaver.h"
#include "CThread.h"
#include "CLog.h"
#include "TMethodJob.h"

#if !defined(SPI_GETSCREENSAVERRUNNING)
#define SPI_GETSCREENSAVERRUNNING 114
#endif

//
// CMSWindowsScreenSaver
//

CMSWindowsScreenSaver::CMSWindowsScreenSaver() :
	m_process(NULL),
	m_threadID(0),
	m_watch(NULL)
{
	// detect OS
	m_is95Family = false;
	m_is95       = false;
	m_isNT       = false;
	OSVERSIONINFO info;
	info.dwOSVersionInfoSize = sizeof(info);
	if (GetVersionEx(&info)) {
		m_is95Family = (info.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS);
		if (info.dwPlatformId   == VER_PLATFORM_WIN32_NT &&
			info.dwMajorVersion <= 4) {
			m_isNT = true;
		}
		else if (info.dwPlatformId  == VER_PLATFORM_WIN32_WINDOWS &&
				info.dwMajorVersion == 4 &&
				info.dwMinorVersion == 0) {
			m_is95 = true;
		}
	}

	// check if screen saver is enabled
	SystemParametersInfo(SPI_GETSCREENSAVEACTIVE, 0, &m_wasEnabled, 0);
}

CMSWindowsScreenSaver::~CMSWindowsScreenSaver()
{
	unwatchProcess();
}

bool
CMSWindowsScreenSaver::checkStarted(UINT msg, WPARAM wParam, LPARAM lParam)
{
	// screen saver may have started.  look for it and get
	// the process.  if we can't find it then assume it
	// didn't really start.  we wait a moment before
	// looking to give the screen saver a chance to start.
	// this shouldn't be a problem since we only get here
	// if the screen saver wants to kick in, meaning that
	// the system is idle or the user deliberately started
	// the screen saver.
	Sleep(250);
	HWND hwnd = findScreenSaver();
	if (hwnd == NULL) {
		// didn't start
		log((CLOG_DEBUG "can't find screen saver window"));
		return false;
	}

	// get the process
	DWORD processID;
	GetWindowThreadProcessId(hwnd, &processID);
	HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, processID);
	if (process == NULL) {
		// didn't start
		log((CLOG_DEBUG "can't open screen saver process"));
		return false;
	}

	// watch for the process to exit
	m_threadID = GetCurrentThreadId();
	m_msg      = msg;
	m_wParam   = wParam;
	m_lParam   = lParam;
	watchProcess(process);

	return true;
}

void
CMSWindowsScreenSaver::enable()
{
	SystemParametersInfo(SPI_SETSCREENSAVEACTIVE, m_wasEnabled, 0, 0);
}

void
CMSWindowsScreenSaver::disable()
{
	SystemParametersInfo(SPI_GETSCREENSAVEACTIVE, 0, &m_wasEnabled, 0);
	SystemParametersInfo(SPI_SETSCREENSAVEACTIVE, FALSE, 0, 0);
}

void
CMSWindowsScreenSaver::activate()
{
	// don't activate if already active
	if (!isActive()) {
		HWND hwnd = GetForegroundWindow();
		if (hwnd != NULL) {
			PostMessage(hwnd, WM_SYSCOMMAND, SC_SCREENSAVE, 0);
		}
		else {
			// no foreground window.  pretend we got the event instead.
			DefWindowProc(NULL, WM_SYSCOMMAND, SC_SCREENSAVE, 0);
		}
	}
}

void
CMSWindowsScreenSaver::deactivate()
{
	bool killed = false;
	if (!m_is95Family) {
		// NT runs screen saver in another desktop
		HDESK desktop = OpenDesktop("screen-saver", 0, FALSE,
								DESKTOP_READOBJECTS | DESKTOP_WRITEOBJECTS);
		if (desktop != NULL) {
			EnumDesktopWindows(desktop,
								&CMSWindowsScreenSaver::killScreenSaverFunc,
								reinterpret_cast<LPARAM>(&killed));
			CloseDesktop(desktop);
		}
	}

	// if above failed or wasn't tried, try the windows 95 way
	if (!killed) {
		// find screen saver window and close it
		HWND hwnd = FindWindow("WindowsScreenSaverClass", NULL);
		if (hwnd != NULL) {
			PostMessage(hwnd, WM_CLOSE, 0, 0);
		}
	}

	// force timer to restart
	SystemParametersInfo(SPI_GETSCREENSAVEACTIVE, 0, &m_wasEnabled, 0);
	SystemParametersInfo(SPI_SETSCREENSAVEACTIVE,
								!m_wasEnabled, 0, SPIF_SENDWININICHANGE);
	SystemParametersInfo(SPI_SETSCREENSAVEACTIVE,
								 m_wasEnabled, 0, SPIF_SENDWININICHANGE);
}

bool
CMSWindowsScreenSaver::isActive() const
{
	if (m_is95) {
		return (FindWindow("WindowsScreenSaverClass", NULL) != NULL);
	}
	else if (m_isNT) {
		// screen saver runs on a separate desktop
		HDESK desktop = OpenDesktop("screen-saver", 0, FALSE, MAXIMUM_ALLOWED);
		if (desktop == NULL && GetLastError() != ERROR_ACCESS_DENIED) {
			// desktop doesn't exist so screen saver is not running
			return false;
		}

		// desktop exists.  this should indicate that the screen saver
		// is running but an OS bug can cause a valid handle to be
		// returned even if the screen saver isn't running (Q230117).
		// we'll try to enumerate the windows on the desktop and, if
		// there are any, we assume the screen saver is running.  (note
		// that if we don't have permission to enumerate then we'll
		// assume that the screen saver is not running.)  that'd be
		// easy enough except there's another OS bug (Q198590) that can
		// cause EnumDesktopWindows() to enumerate the windows of
		// another desktop if the requested desktop has no windows.  to
		// work around that we have to verify that the enumerated
		// windows are, in fact, on the expected desktop.
		CFindScreenSaverInfo info;
		info.m_desktop = desktop;
		info.m_window  = NULL;
		EnumDesktopWindows(desktop,
								&CMSWindowsScreenSaver::findScreenSaverFunc,
								reinterpret_cast<LPARAM>(&info));

		// done with desktop
		CloseDesktop(desktop);

		// screen saver is running if a window was found
		return (info.m_window != NULL);
	}
	else {
		BOOL running;
		SystemParametersInfo(SPI_GETSCREENSAVERRUNNING, 0, &running, 0);
		return (running != FALSE);
	}
}

BOOL CALLBACK
CMSWindowsScreenSaver::findScreenSaverFunc(HWND hwnd, LPARAM arg)
{
	CFindScreenSaverInfo* info = reinterpret_cast<CFindScreenSaverInfo*>(arg);

	if (info->m_desktop != NULL) {
		DWORD threadID = GetWindowThreadProcessId(hwnd, NULL);
		HDESK desktop  = GetThreadDesktop(threadID);
		if (desktop != NULL && desktop != info->m_desktop) {
			// stop enumerating -- wrong desktop
			return FALSE;
		}
	}

	// found a window
	info->m_window = hwnd;

	// don't need to enumerate further
	return FALSE;
}

BOOL CALLBACK
CMSWindowsScreenSaver::killScreenSaverFunc(HWND hwnd, LPARAM arg)
{
	if (IsWindowVisible(hwnd)) {
		PostMessage(hwnd, WM_CLOSE, 0, 0);
		*reinterpret_cast<bool*>(arg) = true;
	}
	return TRUE;
}

HWND
CMSWindowsScreenSaver::findScreenSaver()
{
	if (!m_is95Family) {
		// screen saver runs on a separate desktop
		HDESK desktop = OpenDesktop("screen-saver", 0, FALSE, MAXIMUM_ALLOWED);
		if (desktop != NULL) {
			// search
			CFindScreenSaverInfo info;
			info.m_desktop = desktop;
			info.m_window  = NULL;
			EnumDesktopWindows(desktop,
								&CMSWindowsScreenSaver::findScreenSaverFunc,
								reinterpret_cast<LPARAM>(&info));

			// done with desktop
			CloseDesktop(desktop);

			// return window if found
			if (info.m_window != NULL) {
				return info.m_window;
			}
		}
	}

	// try windows 95 way
	return FindWindow("WindowsScreenSaverClass", NULL);
}

void
CMSWindowsScreenSaver::watchProcess(HANDLE process)
{
	// stop watching previous process
	unwatchProcess();

	// watch new process in another thread
	if (process != NULL) {
		log((CLOG_DEBUG "watching screen saver process"));
		m_process = process;
		m_watch   = new CThread(new TMethodJob<CMSWindowsScreenSaver>(this,
								&CMSWindowsScreenSaver::watchProcessThread));
	}
}

void
CMSWindowsScreenSaver::unwatchProcess()
{
	if (m_watch != NULL) {
		log((CLOG_DEBUG "stopped watching screen saver process"));
		m_watch->cancel();
		m_watch->wait();
		delete m_watch;
		m_watch = NULL;
	}
	if (m_process != NULL) {
		CloseHandle(m_process);
		m_process = NULL;
	}
}

void
CMSWindowsScreenSaver::watchProcessThread(void*)
{
	for (;;) {
		CThread::testCancel();
		if (WaitForSingleObject(m_process, 50) == WAIT_OBJECT_0) {
			// process terminated.  send screen saver deactivation
			// message.
			log((CLOG_DEBUG "screen saver died"));
			PostThreadMessage(m_threadID, m_msg, m_wParam, m_lParam);
			return;
		}
	}
}