/* 
ZLib license:
Copyright (c) 2011 Dirk de la Hunt

This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.

Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:

1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/stat.h>
#ifndef _WIN32
	#include <unistd.h>
	#include <pthread.h>
#endif

#include "monkeybot.h"
#include "downloaditem.h"
#include "utils.h"

#define ACTIONCUSTOM     "custom"
#define ACTIONNO         "no"
#define ACTIONYES        "yes"
#define ACTIONQUIT       "quit"

#define ACTIONLENGTH     "Action is too large."
#define NODATA           "No data returned."
#define MOREINPUT        "Ready for more juicy input."
#define AVAILABLEACTIONS "Apply action? Valid response is \"no\", \"custom:<customaction>\" or \"yes:<action>\", where action is one of the following: \n"
#define ADDGOOD          "Stored."
#define ADDFAIL          "ERROR: Could not add message."
#define UPDATEGOOD       "Updated."
#define UPDATEFAIL       "ERROR: Could not update message."
#define DELETEGOOD       "Message deleted."
#define DELETEFAIL       "ERROR: Could not delete message."
#define SHOWFAIL         "ERROR: Could not show message."
#define SHOWGOOD         "Show finished."
#define SEARCHFAIL       "ERROR: Could not perform search."
#define SEARCHGOOD       "Search finished."

//This is where you define the path to wget for downloads. If it's somewhere in the path environment variable, you needn't worry to edit this.
//Otheriwse put an absolute path here, e.g., c:\tools\wget.exe
#ifdef _WIN32
	#define WGET     "wget.exe"
#else
	#define WGET     "wget"
#endif

typedef struct ActionCombo
{
	Action      action;
	bool        store;
	const char *text;
}
ActionCombo;

static ActionCombo ActionList[] =
{
	{ Action_Store,        true,  "store" },
	{ Action_Download,     true,  "download" },
	{ Action_Todo,         true,  "todo" },
	{ Action_Idea,         true,  "idea" },
	{ Action_Buy,          true,  "buy" },
	{ Action_Remind,       true,  "remind" },
	/* from here on, these messages don't get stored */
	{ Action_Delete,       false, "delete" },
	{ Action_Show  ,       false, "show" },
	{ Action_Search,       false, "search" },
	{ Action_SearchUrls,   false, "searchurls" },
	{ Action_ChangeDB,     false, "changedb" }
};

THREADPROC(MonkeyBot::downloadThread)
{
	DownloadItem *download    = (DownloadItem*)Parameters;
	char         *commandLine = 0;
	int           rc          = -1;
	const char   *path        = 0;
	CrappyString  result;

	if (!download || !download->Input())
		goto done;

	path        = download->Path() ? download->Path() : "./downloads/";
	commandLine = new char[strlen(WGET) + strlen(download->Input()) + 64 + strlen(path)]; /* +64 for commandline options (actually only ~8 characters, but if you mess around with them, this is a lazy way of not caring */
	if (!commandLine)
		goto done;

	sprintf(commandLine, "%s -i %s -P %s", WGET, download->Input(), path);
	rc = system(commandLine);
done:
	if (rc)
		result.MakeString("Download failed: ", download->Url());
	else
		result.MakeString("Download succeeded: ", download->Url());

	download->SendText(result);
	delete download;
	delete [] commandLine;
	return 0;
}

int MonkeyBot::doDb(const char *sqlString, const char *successPhrase, const char *failPhrase, bool showId /*= false*/)
{
	if (!db.Execute(sqlString))
	{
		if (failPhrase)
			SendText(failPhrase);
		else
			SendText(db.Status());
		return -1;
	}

	int   successLength = successPhrase ? strlen(successPhrase) : 0;
	char *temp = new char[successLength + 64];
	int   id = db.LastId();

	if (temp)
	{
		temp[0] = 0;

		if (successPhrase)
			strcpy(temp, successPhrase);

		if (showId)
		{
			char idText[64];
			sprintf(idText, " (messageId: %d)", id);
			strcat(temp, idText);
		}

		if (temp[0])
			SendText(temp);

		delete [] temp;
	}
	else
	{
		SendText("Couldn't allocate memory, something is seriously dodgy.");
	}

	return id;
}

int MonkeyBot::storeMessage(const char *Message)
{
	char *escaped       = db.EscapeString(Message);
	int   escapedLength = escaped ? strlen(escaped) : 0;
	int   id            = -1;
		
	if (!escapedLength)
		return id;

	//add to full-text-search table first to get its ID
	{
		const char *operation = (const char*)"INSERT INTO messages(contents) VALUES('%s')";
		char *sqlString = new char[escapedLength + strlen(operation) + 20]; /* +20 for the decimal length we're going to insert */
		if (!sqlString)
		{
			delete [] escaped;
			return id;
		}
		sprintf(sqlString, operation, escaped, time(0));
		id = doDb(sqlString, 0, 0, false);
		delete [] sqlString;
	}

	//add to message table
	{
		const char *operation = (const char*)"INSERT INTO message(messageId, added, action, showCount, readCount, deleted) VALUES(%d, %d, 'store', 0, 0, 0)";
		char *sqlString = new char[strlen(operation) + 40]; /* +20 for each decimal length we're going to insert */
		if (!sqlString)
		{
			delete [] escaped;
			return -1;
		}
		sprintf(sqlString, operation, id, time(0));
		id = doDb(sqlString, ADDGOOD, ADDFAIL, true);
		delete [] sqlString;
	}

	delete [] escaped;
	return id;
}

void MonkeyBot::updateMessage(int MessageId, const char *Action)
{
	char *escaped = db.EscapeString(Action);
	char  sqlString[256];

	sprintf(sqlString, "UPDATE message SET action='%s' WHERE id=%d", escaped, MessageId);
	doDb(sqlString, /*UPDATEGOOD*/0, UPDATEFAIL, false);
	delete [] escaped;
}

void MonkeyBot::deleteMessage(int MessageId)
{
	char sqlString[128]; /* really only need about ~50ish + ~20ish... I like ishes, they're nice */
	sprintf(sqlString, "UPDATE message SET deleted=deleted+1 WHERE message.id=%d", MessageId);
	doDb(sqlString, DELETEGOOD, DELETEFAIL, false);
}

void MonkeyBot::handleNoAction()
{
	char availableActions[1024];

	strcpy(availableActions, AVAILABLEACTIONS);
	for (int i = 0; i < Action_Count; i++)
	{
		strcat(availableActions, ActionList[i].text);
		if (i + 1 < Action_Count)
			strcat(availableActions, "; ");
	}

	askedForAction = true;
	SendText(availableActions);
}

void MonkeyBot::storeUrl(const char *Url, int MessageId)
{
	char *escaped       = db.EscapeString(Url);
	int   escapedLength = escaped ? strlen(escaped) : 0;
		
	if (!escapedLength)
		return;

	const char *operation = (const char*)"INSERT INTO url(messageId, url, visitedCount, added) VALUES(%d, '%s', 0, %d)";
	char *sqlString = new char[escapedLength + strlen(operation) + 40]; /* +20 per decimal length we're going to insert, because, you know, they can be ~20/~256 long */
	if (!sqlString)
	{
		delete [] escaped;
		return;
	}

	sprintf(sqlString, operation, MessageId, escaped, time(0));
	int id = doDb(sqlString, 0, 0, false);

	delete [] escaped;
	delete [] sqlString;
}

int MonkeyBot::searchCallback(void *Param, int argc, char *argv[])
{
	MonkeyBot *me = (MonkeyBot*)Param;

	int   length = argv[1] ? strlen(argv[1]) : 0;
	char *result = new char[length + 20]; //+20 for decimal index
		
	if (!result)
	{
		me->SendText(argv[0]);
		me->SendText(argv[1]);
	}
	else
	{
		sprintf(result, "%s: %s", argv[0], argv[1]);
		me->SendText(result);
		delete [] result;

	}

	return 0;
}

void MonkeyBot::searchFor(const char *Message, bool OnlyDisplayUrls /*= false*/)
{
	char *escaped       = db.EscapeString(Message);
	int   escapedLength = escaped ? strlen(escaped) : 0;
	char *sqlString     = escapedLength ? new char[escapedLength + 256] : 0; /* +256 to add select statement */

	if (escaped && sqlString)
	{
		if (OnlyDisplayUrls)
		{
			sprintf(sqlString, "SELECT url.messageId, url.url FROM url WHERE url.messageId IN (SELECT message.messageId FROM message INNER JOIN messages ON message.messageId=messages.rowid WHERE contents MATCH '%s')", escaped);
		}
		else
		{
			sprintf(sqlString, "SELECT message.id, messages.contents FROM messages INNER JOIN message ON messages.rowid=message.messageid WHERE contents MATCH '%s'", escaped);
		}
	}

	if (db.Execute(sqlString, searchCallback, this))
	{
		SendText(SEARCHGOOD);
	}
	else
	{
		SendText(db.Status());
		SendText(SEARCHFAIL);
	}

	delete [] escaped;
	delete [] sqlString;
}

int MonkeyBot::showCallback(void *Param, int argc, char *argv[])
{
	MonkeyBot *me = (MonkeyBot*)Param;
	if (argv[0])
		me->SendText(argv[0]);
	else
		me->SendText(NODATA);
	return 0;
}

void MonkeyBot::showMessage(const char *Action)
{
	char  sqlString[256];
	char *zErrMsg = 0;
	int   id = 0;

	if (Action)
		id = atoi(Action); /* FIXME: could do with an isnum check through it */

	if (id > 0)
	{
		sprintf(sqlString, "SELECT contents FROM messages WHERE rowid IN (SELECT messageId FROM message WHERE id=%d)", id);
	}
	else
	{
		if (strcmp(Action, "today") == 0)
		{
			time_t     now;
			struct tm *dayStart;
			now = time(0);
			dayStart = localtime(&now);
			dayStart->tm_hour = 0;
			dayStart->tm_min  = 0;
			dayStart->tm_sec  = 0;
			time_t start = mktime(dayStart);
			time_t end   = start + (60 * 60 * 24);

			//Hmm, this isn't working. It appends start and then 0, fishy. So I must be doing something stoopid.
			//sprintf(sqlString, "SELECT contents FROM messages WHERE rowid IN (SELECT messageId FROM message WHERE added >= %lu AND added <= %lu)", start, end);
			//hacky fixy:
			{
				char temp[20];
				strcpy(sqlString, "SELECT contents FROM messages WHERE rowid IN (SELECT messageId FROM message WHERE added >= ");
				strcat(sqlString, Utils::itoa(start, temp, 10));
				strcat(sqlString, " AND added <= ");
				strcat(sqlString, Utils::itoa(end, temp, 10));
				strcat(sqlString, ");");
			}
		}
		else
		{
			sprintf(sqlString, "SELECT contents FROM messages WHERE rowid IN (SELECT messageId FROM message WHERE action='%s')", Action);
		}
	}

	if (db.Execute(sqlString, showCallback, this))
	{
		SendText(SHOWGOOD);
	}
	else
	{
		SendText(db.Status());
		SendText(SHOWFAIL);
	}
}

void MonkeyBot::processUrls(const char *Message, int MessageId, int ActionId)
{
	char url[4096];
	int  start  = -1;
	int  index  = 0;
	int  length = strlen(Message);
	int  file   = 0;

	while (index < length)
	{
		if (
			((start == -1) && (strncmp(Message + index, "www.", 4) == 0)) ||
			(strncmp(Message + index, "https://", 8) == 0) || 
			(strncmp(Message + index, "http://", 7) == 0) || 
			(strncmp(Message + index, "ftp://", 6) == 0) || 
			(strncmp(Message + index, "svn://", 6) == 0)
			)
		{
			start = index;
		}
		else
		{
			if (start != -1)
			{
				if ((Message[index] == ' ') || (index + 1 == length))
				{
					int count = (index + 1 == length) ? index - start + 1 : index - start;
					strncpy(url, Message + start, count);
					if (url[count-1] == ',' || url[count-1] == '.')
						count--;
					url[count] = 0;
					storeUrl(url, MessageId);
					start = -1;
					if (ActionId == Action_Download)
					{
						char path[1024];
						sprintf(path, "%s%d/", options->DownloadFolder.Buffer(), MessageId);
						Utils::Mkdir(path);

						char filepath[1024];
						sprintf(filepath, "%s%d/%d.txt", options->DownloadFolder.Buffer(), MessageId, file);
						FILE *file = fopen(filepath, "wt");
						if (file)
						{
							fwrite(url, count, 1, file);
							fclose(file);
							DownloadItem *item = new DownloadItem(url, filepath, this, path);
							if (item)
								Utils::CreateThread(downloadThread, (void*)item);
							else
								SendText("Could not start download thread.");
						}
						file++;
					}
				}
			}
		}
		index++;
	}
}


//public
#ifndef __DEBUG
MonkeyBot::MonkeyBot() : ChatEngine()
#else
MonkeyBot::MonkeyBot()
#endif
{
	previousMessage = 0;
	options         = 0;
	memset(&mutex, 0, sizeof(OSMutex));
}

MonkeyBot::~MonkeyBot()
{
	Destroy();
}

bool MonkeyBot::Process(const char *Message)
{
	int messageLength = Message ? strlen(Message) : 0;
	if (messageLength == 0 || strcmp(Message, "\n") == 0)
		return true;

	char         actionText[32];
	const char  *colon     = 0;
	char        *contents  = (char*)Message;
	ActionCombo *action    = 0;
	bool         isCustom  = false;
	int          index;

	memset(actionText, 0, 32);

	colon = strstr(contents, ":");
	if (colon)
	{
		int colonPos = colon - contents;
		if (colonPos > 31)
		{
			SendText(ACTIONLENGTH);
			return false;
		}

		strncpy(actionText, contents, colonPos);
		contents += colonPos + 1;
	}

	isCustom = strcmp(actionText, ACTIONCUSTOM) == 0;

	//check for a pending reply, to apply an action on a previous message
	if (askedForAction && strcmp(contents, ACTIONNO) == 0)
	{
		askedForAction = false;
		SendText(MOREINPUT);
		return true;
	}
	else if (strcmp(actionText, ACTIONYES) == 0 || isCustom)
	{
		strcpy(actionText, colon+1);
		contents = previousMessage;
		if (strcmp(actionText, "delete") == 0)
		{
			askedForAction = false;
			deleteMessage(previousId);
			return true;
		}
	}
	else if (strcmp(actionText, "http") == 0 || strcmp(actionText, "https") == 0 || strcmp(actionText, "ftp") == 0 || strcmp(actionText, "svn") == 0)
	{
		contents = (char*)Message;
		actionText[0] = 0;
	}
	else
	{
		if (!askedForAction)
		{
			if (messageLength > previousLength)
			{
				delete [] previousMessage;
				previousMessage = new char[messageLength + 1];
				if (!previousMessage)
				{
					SendText("Could not allocate memory for message.");
					previousLength = 0;
					return false;
				}
			}
			strcpy(previousMessage, Message);
			previousLength = messageLength;
			previousMessage[previousLength] = 0;
		}
	}

	if (!isCustom)
	{
		//find action
		for (index = 0; index < Action_Count; index++)
		{
			if (strcmp(actionText, ActionList[index].text) == 0)
			{
				action = &ActionList[index];
				break;
			}
		}
	}

	if (!askedForAction && action && action->store)
	{
		/* store the message, either updating or deleting it later */
		previousId = storeMessage(contents);
	}

	if (!action && !isCustom)
	{
		handleNoAction();
	}
	else
	{
		updateMessage(previousId, actionText);
		askedForAction = false;

		switch (action->action)
		{
			case Action_ChangeDB:
			{
				db.Create(contents, true);
				break;
			}
			case Action_Search:
			{
				searchFor(contents);
				break;
			}
			case Action_SearchUrls:
			{
				searchFor(contents, true);
				break;
			}
			case Action_Show:
			{
				showMessage(contents);
				break;
			}
			case Action_Delete:
			{
				int deleteId = contents ? atoi(contents) : -1;
				deleteMessage(deleteId);
				break;
			}
			default:
			{
				processUrls(contents, previousId, action->action);			
				break;
			}
		}
		previousId = -1;
	}
	return true;
}

#ifndef __DEBUG
void MonkeyBot::handleMessage(const Message& msg, MessageSession *session)
{
	JID         jid      = msg.from();
	const char *username = jid.full().c_str();
	long        nowish   = Utils::GetSeconds();

	if (options->Handshake.StartsWith(msg.body().c_str()))
	{
		handshakeTime = nowish;
		SendText(options->HandshakeConfirm);
		return;
	}

	if ((nowish - handshakeTime > 60 * 15) || (!options->God.StartsWith(username)))
	{
		//SendText("Go away."); /* if we stay quiet, there's no give-away that we're a bot */
		return;
	}

	handshakeTime = nowish;

	if (msg.body() == ACTIONQUIT)
	{
		quitting = true;
		client->disconnect();
		return;
	}

	Process(msg.body().c_str());
}

void MonkeyBot::SendText(const char *Message)
{
	Utils::LockMutex(&mutex);
	session->send(Message, "");
	Utils::UnlockMutex(&mutex);
}
#else
void MonkeyBot::SendText(const char *Message)
{
	printf("\nBOT: [%s]\n\n", Message);
}
#endif

bool MonkeyBot::Create(MonkeyOptions *Options)
{
	Destroy();

	options = Options;
	if (!options)
		return Destroy();

	handshakeTime   = 0;
	previousMessage = 0;
	previousLength  = 0;
	askedForAction  = false;

	Utils::Mkdir(options->DownloadFolder);

	if (!db.Create(options->Database, true))
		return Destroy();

	Utils::InitMutex(&mutex);

	return true;
}

bool MonkeyBot::Destroy()
{
	if (options)
	{
		delete options;
		options = 0;
	}

	db.Destroy();

	Utils::FreeMutex(&mutex);
	memset(&mutex, 0, sizeof(OSMutex));

	delete [] previousMessage;
	previousMessage = 0;
	previousLength  = 0;

	return false;
}

void MonkeyBot::Run()
{
#ifdef __DEBUG
	char message[4096];
	while (true)
	{
		fgets(message, sizeof(message), stdin);
		message[strlen(message)-1] = 0;
		if (strcmp(message, ACTIONQUIT) == 0)
			break;
		Process(message);
	}
#else
	Start(options);
#endif
}

