/* Copyright 2010-2014 Fredrik Wikstrom. All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
**    notice, this list of conditions and the following disclaimer.
**
** 2. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS'
** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
** ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
** LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
** CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
** SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
** INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
** CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
** ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
** POSSIBILITY OF SUCH DAMAGE.
*/

#include <proto/exec.h>
#include <proto/dos.h>
#include <proto/utility.h>
#include <proto/icon.h>
#include <proto/wb.h>
#include <proto/commodities.h>
#include <proto/intuition.h>
#include <devices/timer.h>
#include <classes/requester.h>
#include "AutoRun_rev.h"

#define PROGNAME "AutoRun"

#define USE_DOSLIST_NOTIFY 0
#define USE_TIMER 1

static CONST TEXT USED verstag[] = VERSTAG;

#define MAX_AUTORUNS 16

#define POLL_INTERVAL 1000000

#define EXECUTE_CMD "C:Execute"
#define AUTORUN_SCRIPT ":S/AutoRun"
#define DEFAULT_AUTORUN_SCRIPT "SYS:S/AutoRun-Default"

struct AutoRunNode {
	struct AVLNode node;
	BPTR volume;
	struct DateStamp date;
};

struct Library *CxBase;
struct CommoditiesIFace *ICommodities;

static int32 AutoRun = FALSE;
static int32 Confirm = FALSE;

static int32 CxPriority = 0;
static struct MsgPort *CxPort;
static CxObj *CxBroker;

#if USE_DOSLIST_NOTIFY
static int32 DosListSignal = -1;
static int32 DosListNotify = FALSE;
#endif

#if USE_TIMER
static struct MsgPort *TimerPort;
static struct TimeRequest *TimerIO;
static int32 TimerBusy = FALSE;
#endif

static APTR ItemPool;

static int32 read_settings(void);

static int32 register_cx_broker(void);
static void unregister_cx_broker(void);

#if USE_DOSLIST_NOTIFY
static int32 register_doslist_notify(void);
static void unregister_doslist_notify(void);
#endif

#if USE_TIMER
static int32 open_timer_device(void);
static void close_timer_device(void);
static void start_timer_request(uint32 usecs);
static void stop_timer_request(void);
#endif

static void update_volume_tree(void);
static void free_volume_tree(void);

static BPTR file_exists(CONST_STRPTR filename);
static int32 get_autorun_confirmation(BPTR root_lock);
static void execute_command(CONST_STRPTR command);

int main(void) {
	int32 done = FALSE;
	uint32 timer_signal = 0;
	uint32 doslist_signal = 0;
	uint32 cx_signal = 0;
	uint32 signals, wait_mask;
	int rc = RETURN_FAIL;

	CxBase = IExec->OpenLibrary("commodities.library", 53);
	ICommodities = (struct CommoditiesIFace *)IExec->GetInterface(CxBase, "main", 1, NULL);
	if (ICommodities == NULL) {
		goto leave;
	}

	if (read_settings() == FALSE) {
		goto leave;
	}

	if (register_cx_broker() == FALSE) {
		goto leave;
	}

	ItemPool = IExec->AllocSysObjectTags(ASOT_ITEMPOOL,
		ASOITEM_MFlags,    MEMF_PRIVATE,
		ASOITEM_ItemSize,  sizeof(struct AutoRunNode),
		ASOITEM_BatchSize, 16,
		TAG_END);
	if (ItemPool == NULL) {
		goto leave;
	}

#if USE_TIMER
	if (open_timer_device() == FALSE) {
		goto leave;
	}
#endif

#if USE_DOSLIST_NOTIFY
	if (register_doslist_notify() == FALSE) {
		goto leave;
	}
#endif

	update_volume_tree();

	ICommodities->ActivateCxObj(CxBroker, AutoRun = TRUE);

	cx_signal = 1UL << CxPort->mp_SigBit;
#if USE_TIMER
	timer_signal = 1UL << TimerPort->mp_SigBit;
#endif
#if USE_DOSLIST_NOTIFY
	doslist_signal = 1UL << DosListSignal;
#endif

	wait_mask = cx_signal|timer_signal|doslist_signal|SIGBREAKF_CTRL_C;

#if USE_TIMER
	start_timer_request(POLL_INTERVAL);
#endif

	while (done == FALSE) {
		signals = IExec->Wait(wait_mask);

		if (signals & SIGBREAKF_CTRL_C) {
			done = TRUE;
		}

		if (signals & (timer_signal|doslist_signal)) {
#if USE_TIMER
			stop_timer_request();
#endif
			update_volume_tree();
#if USE_TIMER
			start_timer_request(POLL_INTERVAL);
#endif
		}

		if (signals & cx_signal) {
			CxMsg *cx_msg;

			while ((cx_msg = (CxMsg *)IExec->GetMsg(CxPort))) {
				if (ICommodities->CxMsgType(cx_msg) == CXM_COMMAND) {
					switch (ICommodities->CxMsgID(cx_msg)) {
					case CXCMD_DISABLE:
						ICommodities->ActivateCxObj(CxBroker, AutoRun = FALSE);
						break;
					case CXCMD_ENABLE:
						ICommodities->ActivateCxObj(CxBroker, AutoRun = TRUE);
						break;
					case CXCMD_UNIQUE:
					case CXCMD_KILL:
						done = TRUE;
						break;
					}
				}
				IExec->ReplyMsg((struct Message *)cx_msg);
			}
		}
	}

#if USE_TIMER
	stop_timer_request();
#endif

leave:
#if USE_DOSLIST_NOTIFY
	unregister_doslist_notify();
#endif

#if USE_TIMER
	close_timer_device();
#endif

	free_volume_tree();
	IExec->FreeSysObject(ASOT_ITEMPOOL, ItemPool);

	if (ICommodities != NULL) {
		unregister_cx_broker();
	}

	IExec->DropInterface((struct Interface *)ICommodities);
	IExec->CloseLibrary(CxBase);

	return rc;
}

static int32 read_settings(void) {
	struct Library *iconbase;
	struct IconIFace *iicon;
	struct DiskObject *icon;
	CONST_STRPTR tt;
	int32 result = FALSE;

	iconbase = IExec->OpenLibrary("icon.library", 53);
	iicon = (struct IconIFace *)IExec->GetInterface(iconbase, "main", 1, NULL);
	if (iicon == NULL) {
		IExec->CloseLibrary(iconbase);
		return FALSE;
	}

	icon = iicon->GetDiskObject("PROGDIR:"PROGNAME);
	if (icon != NULL) {
		result = TRUE;

		if ((tt = iicon->FindToolType(icon->do_ToolTypes, "CONFIRM"))) {
			if (IUtility->Stricmp(tt, "NO") != 0 &&
				IUtility->Stricmp(tt, "FALSE") != 0 &&
				IUtility->Stricmp(tt, "OFF") != 0)
			{
				Confirm = TRUE;
			}
		}

		if ((tt = iicon->FindToolType(icon->do_ToolTypes, "CX_PRIORITY"))) {
			IDOS->StrToLong(tt, &CxPriority);
		}

		iicon->FreeDiskObject(icon);
	}

	IExec->DropInterface((struct Interface *)iicon);
	IExec->CloseLibrary(iconbase);

	return result;
}

static int32 register_cx_broker(void) {
	struct NewBroker new_broker;

	CxPort = IExec->AllocSysObject(ASOT_PORT, NULL);
	if (CxPort == NULL) {
		return FALSE;
	}

	IUtility->ClearMem(&new_broker, sizeof(new_broker));

	new_broker.nb_Version = NB_VERSION;
	new_broker.nb_Name    = PROGNAME;
	new_broker.nb_Title   = PROGNAME;
	new_broker.nb_Descr   = "Executes a script on disk insertion";
	new_broker.nb_Unique  = NBU_UNIQUE|NBU_NOTIFY;
	new_broker.nb_Flags   = 0;
	new_broker.nb_Pri     = CxPriority;
	new_broker.nb_Port    = CxPort;

	CxBroker = ICommodities->CxBroker(&new_broker, NULL);
	if (CxBroker == NULL) {
		return FALSE;
	}

	return TRUE;
}

static void unregister_cx_broker(void) {
	if (CxBroker != NULL) {
		struct Message *msg;

		ICommodities->DeleteCxObjAll(CxBroker);

		while ((msg = IExec->GetMsg(CxPort))) {
			IExec->ReplyMsg(msg);
		}
	}

	IExec->FreeSysObject(ASOT_PORT, CxPort);
}

#if USE_DOSLIST_NOTIFY
static int32 register_doslist_notify(void) {
	DosListSignal = IExec->AllocSignal(-1);
	if (DosListSignal != -1) {
		DosListNotify = IDOS->NotifyDosListChange(NULL, DosListSignal, 0);
	}
	return DosListNotify;
}

static void unregister_doslist_notify(void) {
	if (DosListNotify) {
		IDOS->NotifyDosListChange(NULL, NDLC_END, 0);
	}

	if (DosListSignal != -1) {
		IExec->FreeSignal(DosListSignal);
	}
}
#endif

#if USE_TIMER
static int32 open_timer_device(void) {
	TimerPort = IExec->AllocSysObject(ASOT_PORT, NULL);
	TimerIO = IExec->AllocSysObjectTags(ASOT_IOREQUEST,
		ASOIOR_ReplyPort, TimerPort,
		ASOIOR_Size,      sizeof(*TimerIO),
		TAG_END);
	if (TimerIO == NULL) {
		return FALSE;
	}

	if (IExec->OpenDevice("timer.device", UNIT_MICROHZ,
		(struct IORequest *)TimerIO, 0) != IOERR_SUCCESS)
	{
		TimerIO->Request.io_Device = NULL;
		return FALSE;
	}

	return TRUE;
}

static void close_timer_device(void) {
	if (TimerIO && TimerIO->Request.io_Device) {
		IExec->CloseDevice((struct IORequest *)TimerIO);
	}

	IExec->FreeSysObject(ASOT_IOREQUEST, TimerIO);
	IExec->FreeSysObject(ASOT_PORT, TimerPort);
}

static void start_timer_request(uint32 usecs) {
	if (TimerBusy == FALSE) {
		TimerIO->Request.io_Command = TR_ADDREQUEST;
		TimerIO->Time.Seconds       = usecs / 1000000UL;
		TimerIO->Time.Microseconds  = usecs % 1000000UL;

		IExec->SendIO((struct IORequest *)TimerIO);

		TimerBusy = TRUE;
	}
}

static void stop_timer_request(void) {
	if (TimerBusy) {
		if (IExec->CheckIO((struct IORequest *)TimerIO) == FALSE) {
			IExec->AbortIO((struct IORequest *)TimerIO);
		}

		IExec->WaitIO((struct IORequest *)TimerIO);

		TimerBusy = FALSE;
	}
}
#endif

static int32 AVLNodeCompareFunc (struct AVLNode *avlnode1, struct AVLNode *avlnode2) {
	struct AutoRunNode *node1 = (struct AutoRunNode *)avlnode1;
	struct AutoRunNode *node2 = (struct AutoRunNode *)avlnode2;
	if (node1->volume != node2->volume) {
		return node1->volume - node2->volume;
	}
	return IDOS->CompareDates(&node1->date, &node2->date);
}

static int32 AVLKeyCompareFunc (struct AVLNode *avlnode1, AVLKey key) {
	struct AutoRunNode *node1 = (struct AutoRunNode *)avlnode1;
	struct VolumeNode *volnode = key;
	BPTR volume = MKBADDR(volnode);
	if (node1->volume != volume) {
		return node1->volume - volume;
	}
	return IDOS->CompareDates(&node1->date, &volnode->vn_VolumeDate);
}

static struct AVLNode *VolumeTree;

static void update_volume_tree(void) {
	struct AVLNode *NewVolumeTree = NULL;
	uint32 doslist_flags = LDF_VOLUMES|LDF_READ;
	struct DosList *doslist;
	struct MsgPort *port;
	int32 is_filesystem;
	struct AVLNode *node;
	struct AutoRunNode *new_node;
	uint32 root_path[1];
	BPTR lock;
	BPTR autorun_locks[MAX_AUTORUNS];
	int32 num_autorun_locks = 0;
	int32 i;
	BPTR old_lock;

	IDOS->CopyStringCToBSTR(":", MKBADDR(root_path), sizeof(root_path));

	doslist = IDOS->LockDosList(doslist_flags);
	while ((doslist = IDOS->NextDosEntry(doslist, doslist_flags))) {
		port = doslist->dol_Port;
		if (port == NULL) {
			continue;
		}

		if (LIB_IS_AT_LEAST(IDOS->Data.LibBase, 53, 87)) {
			is_filesystem = IDOS->IsFileSystemPort(port);
		} else {
			is_filesystem = IDOS->DoPkt0(port, ACTION_IS_FILESYSTEM);
		}
		if (is_filesystem == FALSE) {
			continue;
		}

		node = IExec->AVL_FindNode(VolumeTree, doslist, AVLKeyCompareFunc);
		if (node) {
			IExec->AVL_RemNodeByAddress(&VolumeTree, node);
			IExec->AVL_AddNode(&NewVolumeTree, node, AVLNodeCompareFunc);
		} else {
			if (AutoRun && num_autorun_locks == MAX_AUTORUNS) {
				continue;
			}

			new_node = IExec->ItemPoolAlloc(ItemPool);
			if (new_node == NULL) {
				continue;
			}

			if (AutoRun) {
				lock = IDOS->DoPkt3(port, ACTION_LOCATE_OBJECT, ZERO, MKBADDR(root_path), SHARED_LOCK);
				if (lock == ZERO) {
					IExec->ItemPoolFree(ItemPool, new_node);
					continue;
				}

				autorun_locks[num_autorun_locks++] = lock;
			}

			new_node->volume = MKBADDR(doslist);
			new_node->date = doslist->dol_misc.dol_volume.dol_VolumeDate;

			IExec->AVL_AddNode(&NewVolumeTree, &new_node->node, AVLNodeCompareFunc);
		}
	}
	IDOS->UnLockDosList(doslist_flags);

	free_volume_tree();
	VolumeTree = NewVolumeTree;

	if (AutoRun) {
		for (i = 0; i < num_autorun_locks; i++) {
			lock = autorun_locks[i];
			old_lock = IDOS->CurrentDir(lock);

			if (file_exists(AUTORUN_SCRIPT) && get_autorun_confirmation(lock)) {
				execute_command(EXECUTE_CMD" "AUTORUN_SCRIPT);
			} else if (file_exists(DEFAULT_AUTORUN_SCRIPT)) {
				execute_command(EXECUTE_CMD" "DEFAULT_AUTORUN_SCRIPT);
			}

			IDOS->CurrentDir(old_lock);
			IDOS->UnLock(lock);
		}
	}
}

static void free_volume_tree(void) {
	struct AVLNode *node;

	while ((node = VolumeTree)) {
		IExec->AVL_RemNodeByAddress(&VolumeTree, node);
		IExec->ItemPoolFree(ItemPool, node);
	}
}

static BPTR file_exists(CONST_STRPTR filename) {
	BPTR file;
	IDOS->Close(file = IDOS->Open(filename, MODE_OLDFILE));
	return file;
}

static int32 get_autorun_confirmation(BPTR root_lock) {
	int32 result = FALSE;

	if (Confirm == FALSE) {
		result = TRUE;
	} else {
		struct Library *intuitionbase;
		struct IntuitionIFace *iintuition;

		intuitionbase = IExec->OpenLibrary("intuition.library", 53);
		iintuition = (struct IntuitionIFace *)IExec->GetInterface(intuitionbase, "main", 1, NULL);
		if (iintuition != NULL) {
			struct ClassLibrary *requesterbase;
			Class *requesterclass;

			requesterbase = iintuition->OpenClass("requester.class", 53, &requesterclass);
			if (requesterbase != NULL) {
				TEXT volume_name[256];
				Object *requester;
				CONST_STRPTR varargs[1] = { volume_name };

				if (IDOS->NameFromLock(root_lock, volume_name, sizeof(volume_name)) == FALSE) {
					IUtility->Strlcpy(volume_name, "???", sizeof(volume_name));
				}

				requester = iintuition->NewObject(requesterclass, NULL,
					REQ_Image,      REQIMAGE_QUESTION,
					REQ_TitleText,  VERS,
					REQ_BodyText,   "Volume %s\n"
							        "has an AutoRun script.\n"
							        "Do you want to run it?",
					REQ_VarArgs,    varargs,
					REQ_GadgetText, "Yes|No",
					TAG_END);
				if (requester) {
					struct orRequest openreq;

					openreq.MethodID = RM_OPENREQ;
					openreq.or_Window = NULL;
					openreq.or_Screen = NULL;
					openreq.or_Attrs = NULL;

					result = iintuition->IDoMethodA(requester, (APTR)&openreq);

					iintuition->DisposeObject(requester);
				}

				iintuition->CloseClass(requesterbase);
			}
			IExec->DropInterface((struct Interface *)iintuition);
		}
		IExec->CloseLibrary(intuitionbase);
	}

	return result;
}

static void execute_command(CONST_STRPTR command) {
	struct Library *workbenchbase;
	struct WorkbenchIFace *iworkbench;
	BPTR path_list = ZERO;
	BPTR input_file = ZERO;
	BPTR output_file = ZERO;
	int32 error = -1;

	workbenchbase = IExec->OpenLibrary("workbench.library", 53);
	iworkbench = (struct WorkbenchIFace *)IExec->GetInterface(workbenchbase, "main", 1, NULL);

	if (iworkbench != NULL) {
		iworkbench->WorkbenchControl(NULL,
			WBCTRLA_DuplicateSearchPath, &path_list,
			TAG_END);
	}

	input_file = IDOS->Open("NIL:", MODE_OLDFILE);
	output_file = IDOS->Open("NIL:", MODE_OLDFILE);

	if (input_file && output_file) {
		error = IDOS->SystemTags(command,
			NP_Name,    "AutoRun Script",
			NP_Path,    path_list,
			SYS_Asynch, TRUE,
			SYS_Input,  input_file,
			SYS_Output, output_file,
			SYS_Error,  ZERO,
			TAG_END);
	}

	if (error) {
		IDOS->Close(input_file);
		IDOS->Close(output_file);
	}

	if (error && iworkbench != NULL) {
		iworkbench->WorkbenchControl(NULL,
			WBCTRLA_FreeSearchPath, path_list,
			TAG_END);
	}

	IExec->DropInterface((struct Interface *)iworkbench);
	IExec->CloseLibrary(workbenchbase);
}

