/*

'AutoShutdown'-Plugin for Miranda IM

Copyright 2004-2007 H. Herkenrath

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 (Shutdown-License.txt); if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/

#include "stdafx.h"

/* Msg Shutdown */
static HANDLE hHookEventAdded;
/* Transfer Shutdown */
static HANDLE hHookProtoAck;
/* Idle Shutdown */
static HANDLE hHookIdleChanged;
/* Status Shutdown */
static HANDLE hHookSettingChanged;
/* Weather Shutdown */
static HANDLE hHookWeatherUpdated;
/* Services */
static HANDLE hServiceStartWatcher, hServiceStopWatcher, hServiceIsEnabled;
static HANDLE hEventWatcherChanged;
/* Misc */
static HANDLE hHookModulesLoaded;

/************************* Shared *************************************/

static WORD currentWatcherType;

static void __stdcall MainThreadMapping(void *param)
{
	HANDLE *phDoneEvent = (HANDLE*)param;
	ServiceShutdown(0, TRUE); /* ensure main thread (for cpu usage shutdown) */
	ServiceStopWatcher(0, 0);
	if (*phDoneEvent != NULL)
		SetEvent(*phDoneEvent);
}

static void __inline ShutdownAndStopWatcher(void)
{
	HANDLE hDoneEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
	CallFunctionAsync(MainThreadMapping, &hDoneEvent);
	if (hDoneEvent != NULL) {
		WaitForSingleObject(hDoneEvent, INFINITE);
		CloseHandle(hDoneEvent);
	}
}

/************************* Msg Shutdown *******************************/

// ppBlob might get reallocated, must have been allocated using mir_alloc()
static TCHAR* GetMessageText(BYTE **ppBlob, DWORD *pcbBlob)
{
	(*ppBlob)[*pcbBlob] = 0;
	size_t cb = mir_strlen((char*)*ppBlob);
	/* use Unicode data if present */
	if (*pcbBlob > (cb + 3)) {
		(*ppBlob)[*pcbBlob - 1] = 0;
		return (WCHAR*)&(*ppBlob)[cb];
	}
	/* no Unicode data present, convert from ANSI */
	int len = MultiByteToWideChar(CP_ACP, 0, (char*)*ppBlob, -1, NULL, 0);
	if (!len)
		return NULL;

	BYTE *buf = (BYTE*)mir_realloc(*ppBlob, (*pcbBlob) + (len*sizeof(WCHAR)));
	if (buf == NULL)
		return NULL;

	*pcbBlob += len*sizeof(WCHAR);
	*ppBlob = buf;
	buf = &(*ppBlob)[cb];
	MultiByteToWideChar(CP_ACP, 0, (char*)*ppBlob, -1, (WCHAR*)buf, len);
	((WCHAR*)buf)[len - 1] = 0;
	return (WCHAR*)buf;
}

static int MsgEventAdded(WPARAM, LPARAM hDbEvent)
{
	if (currentWatcherType & SDWTF_MESSAGE) {
		DBEVENTINFO dbe = { sizeof(dbe) };
		dbe.cbBlob = db_event_getBlobSize(hDbEvent);
		dbe.pBlob = (BYTE*)mir_alloc(dbe.cbBlob + 2); /* ensure term zero */
		if (dbe.pBlob == NULL)
			return 0;
		if (!db_event_get(hDbEvent, &dbe))
			if (dbe.eventType == EVENTTYPE_MESSAGE && !(dbe.flags & DBEF_SENT)) {
				DBVARIANT dbv;
				if (!db_get_ts(NULL, "AutoShutdown", "Message", &dbv)) {
					TrimString(dbv.ptszVal);
					TCHAR *pszMsg = GetMessageText(&dbe.pBlob, &dbe.cbBlob);
					if (pszMsg != NULL && _tcsstr(pszMsg, dbv.ptszVal) != NULL)
						ShutdownAndStopWatcher(); /* msg with specified text recvd */
					mir_free(dbv.ptszVal); /* does NULL check */
				}
			}
		mir_free(dbe.pBlob);
	}
	return 0;
}

/************************* Transfer Shutdown **************************/

static HANDLE *transfers;
static int nTransfersCount;

static int ProtoAck(WPARAM, LPARAM lParam)
{
	ACKDATA *ack = (ACKDATA*)lParam;
	if (ack->type != ACKTYPE_FILE)
		return 0;

	switch (ack->result) {
	case ACKRESULT_DATA:
		{
			for (int i = 0; i < nTransfersCount; ++i)
				if (transfers[i] == ack->hProcess)
					break; /* already in list */
			/* insert into list */
			HANDLE *buf = (HANDLE*)mir_realloc(transfers, (nTransfersCount + 1)*sizeof(HANDLE));
			if (buf != NULL) {
				transfers = buf;
				transfers[nTransfersCount] = ack->hProcess;
				++nTransfersCount;
			}
			break;
		}
	case ACKRESULT_SUCCESS:
	case ACKRESULT_FAILED:
	case ACKRESULT_DENIED:
		for (int i = 0; i < nTransfersCount; ++i) {
			if (transfers[i] == ack->hProcess) {
				/* remove from list */
				if (i < (nTransfersCount - 1))
					memmove(&transfers[i], &transfers[i + 1], (nTransfersCount - i - 1)*sizeof(HANDLE));
				--nTransfersCount;
				HANDLE *buf = (HANDLE*)mir_realloc(transfers, nTransfersCount*sizeof(HANDLE));
				if (buf != NULL) transfers = buf;
				else if (!nTransfersCount) transfers = NULL;
				/* stop watcher */
				if (!nTransfersCount && (currentWatcherType&SDWTF_FILETRANSFER))
					ShutdownAndStopWatcher();
				break;
			}
		}
		break;
	}
	return 0;
}

/************************* Idle Shutdown ******************************/

static int IdleChanged(WPARAM, LPARAM lParam)
{
	if (currentWatcherType&SDWTF_IDLE && lParam&IDF_ISIDLE)
		ShutdownAndStopWatcher();
	return 0;
}

/************************* Status Shutdown ****************************/

static BOOL CheckAllContactsOffline(void)
{
	BOOL fSmartCheck, fAllOffline = TRUE; /* tentatively */
	fSmartCheck = db_get_b(NULL, "AutoShutdown", "SmartOfflineCheck", SETTING_SMARTOFFLINECHECK_DEFAULT);
	for (MCONTACT hContact = db_find_first(); hContact; hContact = db_find_next(hContact)) {
		char *pszProto = GetContactProto(hContact);
		if (pszProto != NULL && CallProtoService(pszProto, PS_GETSTATUS, 0, 0) != ID_STATUS_OFFLINE) {
			if (db_get_b(hContact, pszProto, "ChatRoom", 0)) continue;
			if (db_get_w(hContact, pszProto, "Status", 0) != ID_STATUS_OFFLINE) {
				if (fSmartCheck) {
					if (db_get_b(hContact, "CList", "Hidden", 0)) continue;
					if (db_get_b(hContact, "CList", "NotOnList", 0)) continue;
				}
				fAllOffline = FALSE;
				break;
			}
		}
	}
	return fAllOffline;
}

static int StatusSettingChanged(WPARAM wParam, LPARAM lParam)
{
	if (currentWatcherType&SDWTF_STATUS) {
		DBCONTACTWRITESETTING *dbcws = (DBCONTACTWRITESETTING*)lParam;
		if ((HANDLE)wParam != NULL && dbcws->value.wVal == ID_STATUS_OFFLINE && !strcmp(dbcws->szSetting, "Status")) {
			char *pszProto = GetContactProto(wParam);
			if (pszProto != NULL && !strcmp(dbcws->szModule, pszProto))
				if (CheckAllContactsOffline())
					ShutdownAndStopWatcher();
		}
	}
	return 0;
}

/************************* Cpu Shutdown *******************************/

static DWORD idCpuUsageThread;

static BOOL CALLBACK CpuUsageWatcherProc(BYTE nCpuUsage, LPARAM lParam)
{
	static BYTE nTimesBelow = 0; /* only one watcher thread */
	/* terminated? */
	if (idCpuUsageThread != GetCurrentThreadId()) {
		nTimesBelow = 0;
		return FALSE; /* stop poll thread */
	}
	/* ignore random peaks */
	if (nCpuUsage < (BYTE)lParam) ++nTimesBelow;
	else nTimesBelow = 0;
	if (nTimesBelow == 3) {
		nTimesBelow = 0;
		ShutdownAndStopWatcher();
		return FALSE; /* stop poll thread */
	}
	return TRUE;
}

/************************* Weather Shutdown ***************************/

static int WeatherUpdated(WPARAM wParam, LPARAM lParam)
{
	char *pszProto = GetContactProto(wParam);
	if ((BOOL)lParam && pszProto != NULL && CallProtoService(pszProto, PS_GETSTATUS, 0, 0) == THUNDER)
		if (db_get_b(NULL, "AutoShutdown", "WeatherShutdown", SETTING_WEATHERSHUTDOWN_DEFAULT))
			ServiceShutdown(SDSDT_SHUTDOWN, TRUE);
	return 0;
}

/************************* Services ***********************************/

INT_PTR ServiceStartWatcher(WPARAM, LPARAM lParam)
{
	/* passing watcherType as lParam is only to be used internally, undocumented */
	if (lParam == 0)
		lParam = (LPARAM)db_get_w(NULL, "AutoShutdown", "WatcherFlags", 0);

	/* invalid flags or empty? */
	if (!(lParam&SDWTF_MASK))
		return 1;

	/* no specific time choice? */
	if (lParam&SDWTF_SPECIFICTIME && !(lParam&SDWTF_ST_MASK))
		return 2;

	if (currentWatcherType == (WORD)lParam)
		return 3;

	if (currentWatcherType != 0) {
		/* Time Shutdown */
		CloseCountdownFrame(); /* fails if not opened */
		/* Cpu Shutdown */
		idCpuUsageThread = 0;
	}
	SetShutdownMenuItem(true);
	SetShutdownToolbarButton(true);
	currentWatcherType = (WORD)lParam;
	NotifyEventHooks(hEventWatcherChanged, TRUE, 0);

	/* Time Shutdown */
	if (currentWatcherType&SDWTF_SPECIFICTIME)
		ShowCountdownFrame(currentWatcherType); /* after modules loaded */
	/* Cpu Shutdown */
	if (currentWatcherType&SDWTF_CPUUSAGE)
		idCpuUsageThread = PollCpuUsage(CpuUsageWatcherProc, (LPARAM)DBGetContactSettingRangedByte(NULL, "AutoShutdown", "CpuUsageThreshold", SETTING_CPUUSAGETHRESHOLD_DEFAULT, 1, 100), 1500);
	/* Transfer Shutdown */
	if (currentWatcherType&SDWTF_FILETRANSFER && !nTransfersCount)
		ShutdownAndStopWatcher();
	/* Status Shutdown */
	if (currentWatcherType&SDWTF_STATUS && CheckAllContactsOffline())
		ShutdownAndStopWatcher();
	return 0;
}

INT_PTR ServiceStopWatcher(WPARAM, LPARAM)
{
	if (currentWatcherType == 0) return 1;

	/* Time Shutdown */
	if (currentWatcherType&SDWTF_SPECIFICTIME)
		CloseCountdownFrame();
	/* Cpu Shutdown */
	idCpuUsageThread = 0;

	currentWatcherType = 0;
	SetShutdownMenuItem(FALSE);
	SetShutdownToolbarButton(FALSE);
	NotifyEventHooks(hEventWatcherChanged, FALSE, 0);
	return 0;
}

INT_PTR ServiceIsWatcherEnabled(WPARAM, LPARAM)
{
	return currentWatcherType != 0;
}

/************************* Misc ***********************************/

void WatcherModulesLoaded(void)
{
	/* Weather Shutdown */
	if (ServiceExists(MS_WEATHER_UPDATE))
		hHookWeatherUpdated = HookEvent(ME_WEATHER_UPDATED, WeatherUpdated);

	/* restore watcher if it was running on last exit */
	if (db_get_b(NULL, "AutoShutdown", "RememberOnRestart", 0) == SDROR_RUNNING) {
		db_set_b(NULL, "AutoShutdown", "RememberOnRestart", 1);
		ServiceStartWatcher(0, 0); /* after modules loaded */
	}
}

void InitWatcher(void)
{
	/* Shared */
	currentWatcherType = 0;
	/* Message Shutdown */
	hHookEventAdded = HookEvent(ME_DB_EVENT_ADDED, MsgEventAdded);
	/* Status Shutdown*/
	hHookSettingChanged = HookEvent(ME_DB_CONTACT_SETTINGCHANGED, StatusSettingChanged);
	/* Idle Shutdown */
	hHookIdleChanged = HookEvent(ME_IDLE_CHANGED, IdleChanged);
	/* Transfer Shutdown */
	transfers = NULL;
	nTransfersCount = 0;
	hHookProtoAck = HookEvent(ME_PROTO_ACK, ProtoAck);
	/* Weather Shutdown */
	hHookWeatherUpdated = NULL;
	/* Services */
	hEventWatcherChanged = CreateHookableEvent(ME_AUTOSHUTDOWN_WATCHERCHANGED);
	hServiceStartWatcher = CreateServiceFunction(MS_AUTOSHUTDOWN_STARTWATCHER, ServiceStartWatcher);
	hServiceStopWatcher = CreateServiceFunction(MS_AUTOSHUTDOWN_STOPWATCHER, ServiceStopWatcher);
	hServiceIsEnabled = CreateServiceFunction(MS_AUTOSHUTDOWN_ISWATCHERENABLED, ServiceIsWatcherEnabled);
}

void UninitWatcher(void)
{
	/* remember watcher if running */
	if (!ServiceStopWatcher(0, 0))
		if (db_get_b(NULL, "AutoShutdown", "RememberOnRestart", SETTING_REMEMBERONRESTART_DEFAULT))
			db_set_b(NULL, "AutoShutdown", "RememberOnRestart", SDROR_RUNNING);

	/* Message Shutdown */
	UnhookEvent(hHookEventAdded);
	/* Status Shutdown*/
	UnhookEvent(hHookSettingChanged);
	/* Idle Shutdown */
	UnhookEvent(hHookIdleChanged);
	/* Transfer Shutdown */
	UnhookEvent(hHookProtoAck);
	mir_free(transfers); /* does NULL check */
	/* Weather Shutdown */
	UnhookEvent(hHookWeatherUpdated); /* does NULL check */
	/* Services */
	DestroyServiceFunction(hServiceStartWatcher);
	DestroyServiceFunction(hServiceStopWatcher);
	DestroyServiceFunction(hServiceIsEnabled);
	DestroyHookableEvent(hEventWatcherChanged);
	/* Misc */
	UnhookEvent(hHookModulesLoaded);
}