#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/ioctl.h>     // for _IOW, a macro required by FSEVENTS_CLONE
#include <sys/types.h>     // for uint32_t and friends, on which fsevents.h relies
#include <unistd.h>
#include <string.h> // memset
//#include <sys/_types.h>     // for uint32_t and friends, on which fsevents.h relies

#include <sys/sysctl.h> // for sysctl, KERN_PROC, etc.
#include <errno.h>

//#include <sys/fsevents.h>

#include "fsevents.h"
#include "colors.h" 
#include <signal.h> // for kill



/**
 *
 * Filemon: A simple but useful fsevents client utilty for OS X and iOS 
 *
 * Author: Jonathan Levin, TWTR: @Morpheus______ http://NewOSXBook.com/
 *
 * For details, q.v. MOXiI 1st Ed, chapter 3 (pp 74-78), or MOXiI II, Volume I, Chapter 5
 *
 * Source is wide open, so you are free to use and abuse, but leaving credit to the original
 * author and the book website (http://NewOSXBook.com/) would be appreciated.
 *
 * New in 2.0:
 *   - Supports filters: Process IDs, names or events
 *   - Supports auto-stop and auto-link
 *   - Color
 * 
 */

int g_dumpArgs =0;

#define BUFSIZE 256 *1024

#define COLOR_OP YELLOW
#define COLOR_PROC BLUE
#define COLOR_PATH CYAN

// Utility functions
static char *
typeToString (uint32_t	Type)
{
	switch (Type)
	{
		case FSE_CREATE_FILE: return      ("Created       ");
		case FSE_DELETE: return           ("Deleted       ");
		case FSE_STAT_CHANGED: return     ("Changed stat  ");
		case FSE_RENAME:       return     ("Renamed       ");
		case FSE_CONTENT_MODIFIED: return ("Modified      ");
		case FSE_CREATE_DIR:	return    ("Created dir   ");
		case FSE_CHOWN:	        return    ("Chowned       ");

		case FSE_EXCHANGE: return            ("Exchanged     "); /* 5 */
		case FSE_FINDER_INFO_CHANGED: return ("Finder Info   "); /* 6 */
		case FSE_XATTR_MODIFIED: return      ("Changed xattr "); /* 9 */
	 	case FSE_XATTR_REMOVED: return       ("Removed xattr "); /* 10 */

		case FSE_DOCID_CREATED: return ("DocID created "); // 11
		case FSE_DOCID_CHANGED: return ("DocID changed "); // 12

		default : return ("?! ");

	}
}

int lastPID = 0;
static char *
getProcName(long pid)
{

  static char procName[4096];
  size_t len = 1000;
  int rc;
  int mib[4];

  // minor optimization
  if (pid != lastPID) 
  {
  memset(procName, '\0', 4096);

        mib[0] = CTL_KERN;
        mib[1] = KERN_PROC;
        mib[2] = KERN_PROC_PID;
        mib[3] = pid;

        if ((rc = sysctl(mib, 4, procName, &len, NULL,0)) < 0)
                {
                perror("trace facility failure, KERN_PROC_PID\n");
                exit(1);
                }

	//printf ("GOT PID: %d and rc: %d -  %s\n", mib[3], rc, ((struct kinfo_proc *)procName)->kp_proc.p_comm);

	lastPID = pid;
   }
         return (((struct kinfo_proc *)procName)->kp_proc.p_comm);


}

int 
doArg(char *arg, int Print)
{
	// Dump an arg value
	unsigned short *argType = (unsigned short *) arg;
	unsigned short *argLen   = (unsigned short *) (arg + 2);
	uint32_t	*argVal = (uint32_t *) (arg+4);
	uint64_t	*argVal64 = (uint64_t *) (arg+4);
	dev_t		*dev;
	char		*str;



	switch (*argType)
		{

		case FSE_ARG_INT64: // This is a timestamp field on the FSEvent
			if (g_dumpArgs) printf ("Arg64: %lld ", *argVal64);
			break;

		case FSE_ARG_STRING:
		 	str = (char *)argVal;
			if (Print) printf("%s ", str);
			break;
			
		case FSE_ARG_DEV:
			dev = (dev_t *) argVal;
			if (g_dumpArgs) printf ("DEV: %d,%d ", major(*dev), minor(*dev)); break;

		case FSE_ARG_MODE:
			if (g_dumpArgs) printf("MODE: %x ", *argVal); break;
		case FSE_ARG_PATH:
			printf ("PATH: " ); break;
		case FSE_ARG_INO:
			if (g_dumpArgs) printf ("INODE: %d ", *argVal); break;
		case FSE_ARG_UID:
			if (g_dumpArgs) printf ("UID: %d ", *argVal); break;
		case FSE_ARG_GID:
			if (g_dumpArgs) printf ("GID: %d ", *argVal); break;
#define FSE_ARG_FINFO    0x000c   // next arg is a packed finfo (dev, ino, mode, uid, gid)
		case FSE_ARG_FINFO:
			printf ("FINFO\n"); break;
		case FSE_ARG_DONE:	if (Print)printf("\n");return 2;

		default:
			printf ("(ARG of type %hd, len %hd)\n", *argType, *argLen);
			exit(0);


		}

	return (4 + *argLen);

}


#define COLOR_OPTION	"-c"
#define COLOR_LONG_OPTION	"--color"

#define FILTER_PROC_OPTION	"-p"
#define FILTER_PROC_LONG_OPTION	"--proc"
#define FILTER_FILE_OPTION	"-f"
#define FILTER_FILE_LONG_OPTION	"--file"
#define FILTER_EVENT_OPTION	"-e"
#define FILTER_EVENT_LONG_OPTION "--event"
#define STOP_OPTION	"-s"
#define STOP_LONG_OPTION	"--stop"
#define LINK_OPTION	"-l"
#define LINK_LONG_OPTION	"--link"

void
usage()
{

	fprintf(stderr,"Usage: %s [options]\n", getprogname());
	fprintf(stderr,"Where [options] are optional, and may be any of:\n");
	fprintf(stderr,"\t" FILTER_PROC_OPTION  "|" FILTER_PROC_LONG_OPTION  "  pid/procname:  filter only this process or PID\n");
	fprintf(stderr,"\t" FILTER_FILE_OPTION  "|" FILTER_FILE_LONG_OPTION  "  string[,string]:        filter only paths containing this string (/ will catch everything)\n");
	fprintf(stderr,"\t" FILTER_EVENT_OPTION "|" FILTER_EVENT_LONG_OPTION " event[,event]: filter only these events\n");
	fprintf(stderr,"\t" STOP_OPTION "|" STOP_LONG_OPTION         ":                auto-stop the process generating event\n");
	fprintf(stderr,"\t" LINK_OPTION "|" LINK_LONG_OPTION         ":                auto-create a hard link to file (prevents deletion by program :-)\n");
	fprintf(stderr,"\t" COLOR_OPTION "|" COLOR_LONG_OPTION " (or set JCOLOR=1 first)\n");



}
// And.. Ze Main


#define MAX_FILTERS	10

int 
interesting_process (int pid, char *Filters[], int NumFilters)
{
	if (!NumFilters) return 1; 
	return 0;
}
	
int
interesting_file (char *FileName, char *Filters[], int NumFilters)
{

	// if no filters - everything is interesting
	if (!NumFilters) return 1; 

	

	
	while (NumFilters > 0)
	{
// fprintf(stderr,"Checking %s vs %s\n", FileName, Filters[NumFilters-1]);
	   if (strstr(FileName, Filters[NumFilters-1])) return 1;
	   NumFilters--;

	}

	return 0;
}

int
main (int argc, char **argv)
{
	int fsed, cloned_fsed;
	int i; 
	int rc;
	fsevent_clone_args  clone_args;
        unsigned short *arg_type;
	char buf[BUFSIZE];
	int autostop = 0;
	int autolink = 0;
	char *fileFilters[MAX_FILTERS]= { 0 };
	char *procFilters[MAX_FILTERS]= { 0 };

	int numFileFilters = 0;
	int numProcFilters = 0;
	int color = 0;

	if (getenv("JCOLOR")) color++;
	//if (argc > 1) { if (strcmp(argv[1],"-v") == 0) g_dumpArgs++; }

	int arg = 1;

	for (arg = 1; arg < argc; arg++)
	{
	   if (strcmp(argv[arg], "-h") == 0) { usage(); exit(1);}

	   if ((strcmp(argv[arg], FILTER_PROC_OPTION) == 0) ||
	       (strcmp(argv[arg], FILTER_PROC_LONG_OPTION) == 0))
		{
			if (arg == argc -1)
			{
			   fprintf(stderr, "%s: Option requires an argument\n",
				   argv[arg]); 
			   exit(2);
			}
			numProcFilters++;

			arg++; continue;
		}


	   if ((strcmp(argv[arg], FILTER_EVENT_OPTION) == 0) ||
	       (strcmp(argv[arg], FILTER_EVENT_LONG_OPTION) == 0))
		{
			if (arg == argc -1)
			{
			   fprintf(stderr, "%s: Option requires an argument\n",
				   argv[arg]); 
			   exit(2);
			}

			arg++; continue;
		}

	   if ((strcmp(argv[arg], FILTER_FILE_OPTION) == 0) ||
	       (strcmp(argv[arg], FILTER_FILE_LONG_OPTION) == 0))
		{
			if (arg == argc -1)
			{
			   fprintf(stderr, "%s: Option requires an argument\n",
				   argv[arg]); 
			   exit(2);
			}

			// Got it - add filters, separate by ","

			char *begin = argv[arg+1];
			char *sep = strchr (begin, ',');
			while (sep)
			{
			      *sep = '\0';
			      fprintf(stderr,"Adding File filter %d: %s\n", numFileFilters, begin);
			      fileFilters[numFileFilters++] = strdup(begin);
			      begin = sep + 1;
			      sep = strchr (begin, ',');
			   
			}
			fprintf(stderr,"Adding File filter %d: %s\n", numFileFilters, begin);
		        fileFilters[numFileFilters++] = strdup(begin);
			arg++; continue;
		}


	   if ((strcmp(argv[arg], COLOR_OPTION) == 0) || (strcmp(argv[arg], COLOR_LONG_OPTION) == 0))
		{

			color++;
			continue;
		}
	   if ((strcmp(argv[arg], STOP_OPTION) == 0) || (strcmp(argv[arg], STOP_LONG_OPTION) == 0))
		{
			autostop++;
			continue;
		}

	   if ((strcmp(argv[arg], LINK_OPTION) == 0) || (strcmp(argv[arg], LINK_LONG_OPTION) == 0))
		{
			autolink++;
			 continue;
		}

	   fprintf(stderr, "%s: Unknown option\n", argv[arg]); exit(3);
	}

	// This is for your own good: If autostop is allowed on everything, there is a chance a kill -STOP will be 
	// sent to your own terminal app or SSH.

	if (autostop && (!numFileFilters && !numProcFilters))
	{
		fprintf(stderr,"Error: Cannot allow auto-stopping of processes without either a file or process filter.\nIf you are sure you want to do this, set a null filter\n"); exit(4);
	}
	// Open the device
	fsed = open ("/dev/fsevents", O_RDONLY);

	int8_t	events[FSE_MAX_EVENTS];

	if (geteuid())
	{
		fprintf(stderr,"Opening /dev/fsevents requires root permissions\n");
	}

	if (fsed < 0)
	{
		perror ("open");
		 exit(1);
	}


	// Prepare event mask list. In our simple example, we want everything
	// (i.e. all events, so we say "FSE_REPORT" all). Otherwise, we 
	// would have to specifically toggle FSE_IGNORE for each:
	//
	// e.g. 
	//       events[FSE_XATTR_MODIFIED] = FSE_IGNORE;
	//       events[FSE_XATTR_REMOVED]  = FSE_IGNORE;
	// etc..
	for (i = 0; i < FSE_MAX_EVENTS; i++)
	{
		events[i] = FSE_REPORT; 
	}

	// But in v2.0, we allow user to specify events

	// Get ready to clone the descriptor:

	memset(&clone_args, '\0', sizeof(clone_args));
	clone_args.fd = &cloned_fsed; // This is the descriptor we get back
	clone_args.event_queue_depth = 100; // Makes all the difference
	clone_args.event_list = events;
	clone_args.num_events = FSE_MAX_EVENTS;
	
	// Do it.

	rc = ioctl (fsed, FSEVENTS_CLONE, &clone_args);
	
	if (rc < 0) { perror ("ioctl"); exit(2);}
	
	// We no longer need original..

	close (fsed);

	
	// And now we simply read, ad infinitum (aut nauseam)

	while ((rc = read (cloned_fsed, buf, BUFSIZE)) > 0)
	{
		// rc returns the count of bytes for one or more events:
		int offInBuf = 0;

		while (offInBuf < rc) {
	
		   // printf("----%d/%d bytes\n",offInBuf,rc);
	
		   struct kfs_event_a *fse = (struct kfs_event_a *)(buf + offInBuf);
		   struct kfs_event_arg *fse_arg;


	//		if (offInBuf) { printf ("Next event: %d\n", offInBuf);};

			if (fse->type == FSE_EVENTS_DROPPED)
			{
				printf("Some events dropped\n");
				break;
			}

		   if (!fse->pid){ printf ("%x %x\n", fse->type, fse->refcount); }

		   int print = 0;
		   char *procName = getProcName(fse->pid);
		   offInBuf+= sizeof(struct kfs_event_a);
		   fse_arg = (struct kfs_event_arg *) &buf[offInBuf];

		   if (interesting_process(fse->pid, procFilters, numProcFilters) 
                    && interesting_file (fse_arg->data, fileFilters, numFileFilters))
		   {
		   	printf ("%5d %s%s%s\t%s%s%s ", fse->pid, 
				color ? COLOR_PROC: "" , procName,  color ? NORMAL : "",
				color ? COLOR_OP : "", typeToString(fse->type), color ? NORMAL : "" );

		        // The path name is null terminated, so that's cool
		        printf ("%s%s%s\t", 
				color ? COLOR_PATH : "" , fse_arg->data, color ? NORMAL :"");
		     
  			if (fse->type == FSE_CREATE_FILE && autolink && (fse->pid != getpid()))
			{
			   int fileLen = strlen(fse_arg->data);
			   char *linkName = malloc (fileLen + 20);
			   strcpy(linkName, fse_arg->data);
			   snprintf(linkName + fileLen, fileLen + 20, ".filemon.%d", autolink);
			   int rc = link (fse_arg->data, linkName);
			   if (rc) { fprintf(stderr,"%sWarning: Unable to autolink %s%s - file must have been deleted already\n", 
					color ? RED : "", 
					fse_arg->data,
					color ? NORMAL : "");}

			   else    { fprintf(stderr,"%sAuto-linked %s to %s%s\n", 
					color ? GREEN : "", 
					fse_arg->data, linkName,
					color ? NORMAL :"");
			   		autolink++;
				   }

			   free (linkName);
			}

		   // Autostop only if this is a file creation, and interesting
		   if (autostop && fse->type == FSE_CREATE_FILE ) { 
				  fprintf(stderr, "%sAuto-stopping process %s (%d) on file operation%s\n", 
						color ? RED : "",
						    procName,
						  fse->pid,
						color ? NORMAL : "");
			           kill (fse->pid, SIGSTOP);
				 }

			print = 1;


		   }

	           offInBuf += sizeof(kfs_event_arg) + fse_arg->pathlen ;

		   int arg_len = doArg(buf + offInBuf,print);
	           offInBuf += arg_len;
		   while (arg_len >2 && offInBuf < rc)
			{
		   	    arg_len = doArg(buf + offInBuf, print);
	           	    offInBuf += arg_len;
			}
	
		}
		   memset (buf,'\0', BUFSIZE);
		if (rc > offInBuf) { printf ("*** Warning: Some events may be lost\n"); }
	}

} // end main