/*	motion.c
 *
 *	Detect changes in a video stream.
 *	Copyright 2000 by Jeroen Vreeken (pe1rxq@amsat.org)
 *	This software is distributed under the GNU public license version 2
 *	See also the file 'COPYING'.
 *
 */
#include "motion.h"
#include "conf.h"

extern struct config conf;

int deamon=0;
int snapshot=0;
int alarmtime=0;
int fmt=VIDEO_PALETTE_RGB24;
int read_img=0;

char *start_capture (int dev, int width, int height, int input, int norm)
{
        struct video_capability vid_caps;
	struct video_channel vid_chnl;
	struct video_window vid_win;
	struct video_mbuf vid_buf;
	char *map;

	if (ioctl (dev, VIDIOCGCAP, &vid_caps) == -1) {
		perror ("ioctl (VIDIOCGCAP)");
		return (NULL);
	}
	if (vid_caps.type & VID_TYPE_MONOCHROME) fmt=VIDEO_PALETTE_GREY;
	/* Set channels and norms, NOT TESTED my philips cam doesn't have a
	 * tuner. update: it works with my brothers haupage wintv (bttv driver)
	 */
        if (input != IN_DEFAULT) {
                vid_chnl.channel = -1;
                if (ioctl (dev, VIDIOCGCHAN, &vid_chnl) == -1) {
                        perror ("ioctl (VIDIOCGCHAN)");
                } else {
                        vid_chnl.channel = input;
                        vid_chnl.norm    = norm;
                        if (ioctl (dev, VIDIOCSCHAN, &vid_chnl) == -1) {
                                perror ("ioctl (VIDIOCSCHAN)");
                                return (NULL);
                        }
                }
        }
	if (ioctl (dev, VIDIOCGMBUF, &vid_buf) == -1) {
		fprintf(stderr, "no mmap falling back on read\n");
		if (ioctl (dev, VIDIOCGWIN, &vid_win)== -1) {
			perror ("ioctl VIDIOCGWIN");
			return (NULL);
		}
		vid_win.width=width;
		vid_win.height=height;
		if (ioctl (dev, VIDIOCSWIN, &vid_win)== -1) {
			perror ("ioctl VIDIOCSWIN");
			return (NULL);
		}
		read_img=1;
		map=malloc(width*height*3);
		return (map);
	}
	/* If we are going to capture greyscale we need room to blow the image up */
	if (fmt==VIDEO_PALETTE_GREY)
		map=mmap(0, vid_buf.size*3, PROT_READ|PROT_WRITE, MAP_SHARED, dev, 0);
	else
		map=mmap(0, vid_buf.size, PROT_READ|PROT_WRITE, MAP_SHARED, dev, 0);
	
	if ((unsigned char *)-1 == (unsigned char *)map)
		return (NULL);
	return map;
}

char *next_capture (int dev, char *map, int width, int height)
{
	int i;
	char *grey, *rgb;
	struct video_mmap vid_mmap;

    	sigset_t    set, old;

	if (read_img) {
		if (fmt==VIDEO_PALETTE_GREY) {
			if (read(dev, map, width*height) != width*height)
				return NULL;
		} else {
			if (read(dev, map, width*height*3) != width*height*3)
				return NULL;
		}
	} else {
		vid_mmap.format=fmt;
		vid_mmap.frame=0;
		vid_mmap.width=width;
		vid_mmap.height=height;
    
        	sigemptyset (&set);	     //BTTV hates signals during IOCTL
	        sigaddset (&set, SIGCHLD);   //block SIGCHLD & SIGALRM
       		sigaddset (&set, SIGALRM);   //for the time of ioctls
        	sigprocmask (SIG_BLOCK, &set, &old);
                    
		if (ioctl(dev, VIDIOCMCAPTURE, &vid_mmap) == -1) {
	        	sigprocmask (SIG_UNBLOCK, &old, NULL);
			return (NULL);
		}
		if (ioctl(dev, VIDIOCSYNC, &vid_mmap) == -1) {
	        	sigprocmask (SIG_UNBLOCK, &old, NULL);
			return (NULL);
		}
		
        	sigprocmask (SIG_UNBLOCK, &old, NULL); //undo the signal blocking
	}
	/* Blow up a grey */
	if (fmt==VIDEO_PALETTE_GREY) {
		i=width*height;
		grey=map+i-1;
		rgb=map+i*3;
		for (; i>=0; i--, grey--) {
			*(rgb--)=*grey;
			*(rgb--)=*grey;
			*(rgb--)=*grey;
		}
	}
	return map;
}

void put_jpeg (FILE *picture, char *image, int width, int height, int quality)
{
        int y, x, line_width;
        JSAMPROW row_ptr[1];
        struct jpeg_compress_struct cjpeg;
        struct jpeg_error_mgr jerr;
        unsigned char *line;

        line = malloc (width * 3);
        if (!line)
                return;
        cjpeg.err = jpeg_std_error(&jerr);
        jpeg_create_compress (&cjpeg);
        cjpeg.image_width = width;
        cjpeg.image_height= height;
        cjpeg.input_components = 3;
        cjpeg.in_color_space = JCS_RGB;
        jpeg_set_defaults (&cjpeg);

        jpeg_set_quality (&cjpeg, quality, TRUE);
        cjpeg.dct_method = JDCT_FASTEST;
        jpeg_stdio_dest (&cjpeg, picture);

        jpeg_start_compress (&cjpeg, TRUE);

        row_ptr[0] = line;
        line_width = width * 3;
	for ( y = 0; y < height; y++) {
        for (x = 0; x < line_width; x+=3) {
                        line[x]   = image[x+2];
                        line[x+1] = image[x+1];
                        line[x+2] = image[x];
                }
                jpeg_write_scanlines (&cjpeg, row_ptr, 1);
                image += line_width;
        }
        jpeg_finish_compress (&cjpeg);
        jpeg_destroy_compress (&cjpeg);
        free (line);
}

void put_ppm (FILE *picture, char *image, int width, int height)
{
	int x, y;

	/*	ppm header
	 *	width height
	 *	maxval
	 */
	fprintf(picture, "P6\n");
	fprintf(picture, "%d %d\n", width, height);
	fprintf(picture, "%d\n", 255);
	for (y=0; y<height; y++) {
		for (x=0; x<width; x++) {
			/* ppm is rgb not bgr */
			fwrite(&image[y*width*3+x*3+2], 1, 1, picture);
			fwrite(&image[y*width*3+x*3+1], 1, 1, picture);
			fwrite(&image[y*width*3+x*3+0], 1, 1, picture);
		}
	}
}

char *get_ppm (FILE *picture, int width, int height)
{
	int x=0 ,y=0;
	char line[255];
	char *image;
	fgets(line, 255, picture);
	if (strncmp(line, "P6", 2)) {
		fprintf(stderr, "This is not a ppm file\n");
		return NULL;
	}
	/* skip comment */
	line[0]='#';
	while (line[0]=='#') fgets(line, 255, picture);
	/* check size */
	sscanf(line, "%d %d", &x, &y);
	if (x!=width || y!=height) {
		fprintf(stderr, "Wrong image size\n");
		return NULL;
	}
	/* Maximum value */
	fgets(line, 255, picture);
	sscanf(line, "%d", &x);
	if (x!=255) {
		fprintf(stderr, "Wrong image format\n");
		return NULL;
	}
	/* read data */
	image=malloc(width*height*3);
	for (y=0; y<height; y++) {
		for (x=0; x<width; x++) {
			/* ppm is rgb not bgr */
			fread(&image[y*width*3+x*3+2], 1, 1, picture);
			fread(&image[y*width*3+x*3+1], 1, 1, picture);
			fread(&image[y*width*3+x*3+0], 1, 1, picture);
		}
	}
	return image;
}

void send_sms(char *sms_nr, struct tm *currenttime)
{
	char message[150];

	if (!fork()) {
	    	printf("Sending SMS message to %s\n", sms_nr);
		sprintf(message, "Motion detected: %04d-%02d-%02d %02d:%02d:%02d\n",
		    currenttime->tm_year + 1900, currenttime->tm_mon +1,
		    currenttime->tm_mday, currenttime->tm_hour,
		    currenttime->tm_min, currenttime->tm_sec);
		execlp("sms_client", "sms_client", sms_nr, message, 0);
		perror("Unable to send SMS message");
		exit(0);
	}
	return;
}

void send_mail(char *mail_address, struct tm *currenttime)
{
	char message[150];
	int fdpipe[2];
	
	if (!fork()) {
	    	printf("Sending mail to %s\n", mail_address);
		sprintf(message, "This is an automated message generated by motion\n\n"
		    "Motion detected: %04d-%02d-%02d %02d:%02d:%02d\n"
		    ".\n",
		    currenttime->tm_year + 1900, currenttime->tm_mon +1,
		    currenttime->tm_mday, currenttime->tm_hour,
		    currenttime->tm_min, currenttime->tm_sec);
		pipe(fdpipe);
		if (!deamon) {
			close(0);
			dup(fdpipe[0]);
			close(fdpipe[0]);
		}
		write(fdpipe[1], message, strlen(message));
		close(fdpipe[1]);
		execlp("mail", "mail", mail_address, "-s", "MOTION ALERT", 0);
		perror("Unable to send message");
		exit(1);
	}
	return;
}

void exec_externcommand (char *externcommand)
{
	if (!fork()) {
		printf("Executing external command '%s'\n", externcommand);
		execlp(externcommand, externcommand, 0);
		perror("Unable to start external command");
		exit(1);
	}
	return;
}

void create_mpeg (int event_nr, int filesc, char **files)
{
	int i;
	char params[255];
	char eventname[255];
	char *mpegname=eventname;
	FILE *paramfile=NULL;

	if (!filesc) return;
	strncpy(eventname, files[0], strlen(files[0])-7);
	if (strlen(files[0])==25) {
		eventname[strlen(files[0])-8]='m';
		eventname[strlen(files[0])-7]=0;
	} else {
		eventname[strlen(files[0])-7]=0;
	}
	fprintf(stderr, "%d %s\n", strlen(files[0]),eventname);
	/* Cut of eventnr, we have te date so we don't need to sort */
	while((mpegname++)[0]!='-');
	sprintf(params, "%s.params", mpegname);
	paramfile=fopen(params, "w");
	if (!fork()) {
		printf("Starting mpeg encoding of event %d\n", event_nr);
		fprintf(paramfile,
			"PATTERN IBBPBBPBBPBBPBBP\n"
			"OUTPUT %s.mpg\n"
			"BASE_FILE_FORMAT ", mpegname);
		if (!strncmp("ppm", files[0]+strlen(files[0])-3, 3))
			fprintf(paramfile, "PPM\n");
		else 
			fprintf(paramfile, "JPEG\n");
		fprintf(paramfile,
			"INPUT_CONVERT *\n"
			"GOP_SIZE 30\n"
			"SLICES_PER_FRAME 1\n"
			"INPUT_DIR .\n"
			"INPUT\n");
		for (i=0; i<filesc; i++) fprintf(paramfile, "%s\n", files[i]);
		fprintf(paramfile,
			"END_INPUT\n"
			"PIXEL HALF\n"
			"RANGE 10\n"
			"PSEARCH_ALG LOGARITHMIC\n"
			"BSEARCH_ALG CROSS2\n"
			"IQSCALE 8\n"
			"PQSCALE 10\n"
			"BQSCALE 25\n"
			"FORCE_ENCODE_LAST_FRAME\n"
			"REFERENCE_FRAME ORIGINAL\n"/*DECODED*/
			"FRAME_RATE 25\n");
		fclose(paramfile);
		if (!fork()) {
			nice(5);
			execlp("mpeg_encode", "mpeg_encode", params, 0);
			perror("Failed to start mpeg_encode");
			exit(1);
		}
		if (wait(0)==-1) exit(1);
		for (i=0; i<filesc; i++) {
			remove(files[i]);
			free(files[i]);
		}
		free(files);
		remove(params);
		exit(0);
	}
	return;
}

struct coord locate_diffs_center (char *out, int width, int height)
{
	int x, y, centc=0;
	struct coord cent;
	
	cent.x=0;
	cent.y=0;
	/* Locate movement */	
	for (x=0; x<width; x++) {
		for (y=0; y<height; y++) {
			if (out[(x+y*width)*3+0] && out[(x+y*width)*3+1] && out[(x+y*width)*3+2]) {
				cent.x+=x;
				cent.y+=y;
				centc++;
			}
		}
	}
	cent.x=cent.x/centc;
	cent.y=cent.y/centc;
	return (cent);
}

void locate_diffs (char *out, char *new, int width, int height)
{
	int x, y, maxx=0, maxy=0, minx=width, miny=height;
	int centc=0, xdist=0, ydist=0;
	struct coord cent;
	
	cent=locate_diffs_center (out, width, height);
	centc=0;
	for (x=0; x<width; x++) {
		for (y=0; y<height; y++) {
			if (out[(x+y*width)*3+0] && out[(x+y*width)*3+1] && out[(x+y*width)*3+2]) {
				if (x > cent.x) xdist+=x-cent.x;
				if (y > cent.y) ydist+=y-cent.y;
				if (x < cent.x) xdist+=cent.x-x;
				if (y < cent.y) ydist+=cent.y-y;
				centc++;
			}
		}
	}
	minx=cent.x-xdist/centc*2;
	maxx=cent.x+xdist/centc*2;
	/* Make the box a litle bigger in y direction to fit the heads and
	 * foots in */
	miny=cent.y-ydist/centc*3;
	maxy=cent.y+ydist/centc*3;
	if (minx < 0) minx=0;
	if (miny < 0) miny=0;
	if (maxx > width-1) maxx=width-1;
	if (maxy > height-1) maxy=height-1;
	/* Draw a box around the movement */
	for (x=minx; x<=maxx; x++) {
		new[(x+width*miny)*3+0]=255;
		new[(x+width*miny)*3+1]=255;
		new[(x+width*miny)*3+2]=255;
		new[(x+width*maxy)*3+0]=255;
		new[(x+width*maxy)*3+1]=255;
		new[(x+width*maxy)*3+2]=255;
	}
	for (y=miny; y<=maxy; y++) {
		new[(minx+y*width)*3+0]=255;
		new[(minx+y*width)*3+1]=255;
		new[(minx+y*width)*3+2]=255;
		new[(maxx+y*width)*3+0]=255;
		new[(maxx+y*width)*3+1]=255;
		new[(maxx+y*width)*3+2]=255;
	}
}

void sig_alarm (int signo) 
{
	/* Somebody (maybe we ourself) wants us to make a snapshot */
	snapshot=1;
	/* Set everything for the next snapshot */
	signal(SIGALRM, sig_alarm);
	if (alarmtime) alarm(alarmtime);
	return;
}

int diff_standard (char *ref, char *new, char *out, char *mask, int width, int height)
{
	int i, diffs=0;

	for (i=0; i<width*height*3; i++) {
		out[i]=ref[i]-new[i];
		if (mask) out[i]=((int)((char)out[i]*(unsigned char)mask[i])/255);
		if ((signed char)out[i] > conf.noise ||
		    (signed char)out[i] < -conf.noise ) {
			out[i]=new[i];
			diffs++;
		} else {
			out[i]=0;
		}
	}
	return diffs;
}

int main (int argc, char **argv)
{
	int dev, i, diffs, shots=0, event_nr=0, prev_event=0, filesc=0, filesmc=0;
	unsigned char *image_ref, *image_new, *image_out, *image_mask=NULL;
	time_t currenttimep, lasttime=0, lastshot=0;
	struct tm *currenttime=NULL;
	char file[255], filebase[255], timestamp[255], datestamp[255];
	char **files=NULL, **filesm=NULL;
	FILE *picture;
#ifdef TRACKING
	int moved=0;
#endif

	conf_load(argc, argv);
	
	if (conf.mask_file) {
		if ((picture=fopen(conf.mask_file, "r"))) {
			image_mask=get_ppm(picture, conf.width, conf.height);
			fclose(picture);
		}
		if (!image_mask) {
			perror ("failed to read mask image");
			exit(1);
		}
	}
	
	if (conf.filepath) if (chdir(conf.filepath)) {
		perror("Unable to chdir to target directory");
		exit(1);
	}

	/* Set signal handlers */
	signal(SIGCHLD, SIG_IGN);
	signal(SIGALRM, sig_alarm);

	image_ref=malloc(conf.width*conf.height*3);
	image_out=malloc(conf.width*conf.height*3);

	dev=open (conf.device, O_RDWR);
	if (dev < 0) {
		perror ("Failed to open video device");
		exit(1);
	}

	/* Deamonize */
	if (deamon) {
		close(0);
		close(1);
		close(2);
		if (fork()) exit(0);
	}
	/* set the device settings */
	image_new=start_capture (dev, conf.width, conf.height, conf.input, conf.norm);
	if (!image_new) {
		perror("Capture error");
		exit(1);
	}
	/* Allow videodevice to settle in */
	sleep(1);
	/* Capture first image, or we will get an alarm on start */
	if (!next_capture(dev, image_new, conf.width, conf.height)) {
		perror("Capture error");
		exit(1);
	}
	memcpy(image_ref, image_new, conf.width*conf.height*3);

	if (alarmtime) alarm(alarmtime);
#ifdef TRACKING
	track_center();
#endif
	/* Should go on forever... unless you bought vaporware :) */
	while (next_capture(dev, image_new, conf.width, conf.height)) {
#ifdef TRACKING
		if (moved) {
			for (i=0; i<conf.width*conf.height*3; i++)
				image_ref[i]=image_new[i];
			moved=0;
			continue;
		}
#endif
		/* Make a differences picture in image_out */
		diffs=diff_standard(image_ref, image_new, image_out, image_mask, conf.width, conf.height);
		for (i=0; i<conf.width*conf.height*3; i++) {
			/* Old image slowly decays, this will make it even harder on
			 * slow moving object to stay undetected
			 */
			image_ref[i]=(image_ref[i]+image_new[i])/2;
		}
		currenttimep=time(NULL);
		currenttime=localtime(&currenttimep);
		if (lasttime!=currenttimep) {
			shots=-1;
		}
		if (currenttimep-lasttime>= conf.gap && lasttime) {
			if (event_nr==prev_event) {
#ifdef TRACKING
				track_center();
				moved=1;
#endif
				event_nr++;
    				if (conf.mpeg) {
					create_mpeg(prev_event, filesc, files);
					create_mpeg(prev_event, filesmc, filesm);
				} else {
					for (i=0; i<filesc; i++) free(files[i]);
					free(files);
					for (i=0; i<filesmc; i++) free(filesm[i]);
					free(filesm);
				}
				filesc=0;
				files=NULL;
				filesmc=0;
				filesm=NULL;
			}
		}
		/* The cat just walked in :) */
		if (diffs > conf.max_changes && shots<conf.frame_limit-1 &&
		    currenttimep-lastshot >= conf.mingap ) {
			lastshot=currenttimep;
			shots++;
			/* Output the latest picture 'image_new' or image_out for motion picture.
			 */
			sprintf(filebase, "%02d-%04d%02d%02d%02d%02d%02d-%02d",
			    event_nr,
			    currenttime->tm_year + 1900, currenttime->tm_mon + 1,
			    currenttime->tm_mday, currenttime->tm_hour,
			    currenttime->tm_min, currenttime->tm_sec, shots);
			sprintf(datestamp, "%04d-%02d-%02d",
			    currenttime->tm_year + 1900, currenttime->tm_mon + 1,
			    currenttime->tm_mday);
			sprintf(timestamp, "%02d:%02d:%02d-%02d",
			    currenttime->tm_hour, currenttime->tm_min,
			    currenttime->tm_sec, shots);
			if (conf.motion_img) {
				if (conf.ppm)
					sprintf(file, "%sm.ppm", filebase);
				else
					sprintf(file, "%sm.jpg", filebase);
				printf("Motion picture saved to: %s\n", file);
				picture=fopen(file, "w");
				filesmc++;
				filesm=realloc(filesm, filesmc*(sizeof(void *)));
				filesm[filesmc-1]=malloc(30);
				strncpy(filesm[filesmc-1], file, 30);
				draw_text(image_out, conf.width-70, conf.height-20, conf.height, conf.width, datestamp);
				draw_text(image_out, conf.width-70, conf.height-10, conf.height, conf.width, timestamp);
				if (conf.locate && diffs > conf.max_changes)
					locate_diffs(image_out, image_out, conf.width, conf.height);
				if (conf.ppm)
					put_ppm (picture, image_out, conf.width, conf.height);
				else
					put_jpeg (picture, image_out, conf.width, conf.height, conf.quality);
				fclose(picture);
			} 
			if (conf.new_img) {
				if (conf.ppm)
					sprintf(file, "%s.ppm", filebase);
				else
					sprintf(file, "%s.jpg", filebase);
				printf("Normal picture saved to: %s\n", file);
				filesc++;
				files=realloc(files, filesc*(sizeof(void *)));
				files[filesc-1]=malloc(30);
				strncpy(files[filesc-1], file, 30);
				picture=fopen(file, "w");
				draw_text(image_new, conf.width-70, conf.height-20, conf.height, conf.width, datestamp);
				draw_text(image_new, conf.width-70, conf.height-10, conf.height, conf.width, timestamp);
				if (conf.locate && diffs > conf.max_changes)
					locate_diffs(image_out, image_new, conf.width, conf.height);
				if (conf.ppm)
					put_ppm (picture, image_new, conf.width, conf.height);
				else
					put_jpeg (picture, image_new, conf.width, conf.height, conf.quality);
				fclose(picture);
			}
		}
		if (diffs > conf.max_changes) {
			if (!conf.quiet) printf("\a");
			printf("Motion detected %d\n", diffs);
			/* Take action if necesarry */
			if (event_nr!=prev_event) {
				if (conf.sms_nr) send_sms(conf.sms_nr, currenttime);
				if (conf.mail_address) send_mail(conf.mail_address, currenttime);
				if (conf.externcommand) exec_externcommand(conf.externcommand);
				prev_event=event_nr;
			}
#ifdef TRACKING
			if (track_move(image_out, conf.width, conf.height)) moved=1;
#endif
			lasttime=currenttimep;
		} else if (conf.always_diff) {
			printf("changes: %d\n", diffs);
		}
		if (snapshot) {
			sprintf(filebase, "%04d%02d%02d%02d%02d%02d-snapshot",
			    currenttime->tm_year + 1900, currenttime->tm_mon + 1,
			    currenttime->tm_mday, currenttime->tm_hour,
			    currenttime->tm_min, currenttime->tm_sec);
			sprintf(datestamp, "%04d-%02d-%02d",
			    currenttime->tm_year + 1900, currenttime->tm_mon + 1,
			    currenttime->tm_mday);
			sprintf(timestamp, "%02d:%02d:%02d",
			    currenttime->tm_hour, currenttime->tm_min,
			    currenttime->tm_sec);
			if (conf.ppm)
				sprintf(file, "%s.ppm", filebase);
			else
				sprintf(file, "%s.jpg", filebase);
			printf("Snapshot saved to: %s\n", file);
			draw_text(image_new, conf.width-70, conf.height-20, conf.height, conf.width, datestamp);
			draw_text(image_new, conf.width-70, conf.height-10, conf.height, conf.width, timestamp);
			picture=fopen(file, "w");
			if (conf.ppm)
				put_ppm (picture, image_new, conf.width, conf.height);
			else
				put_jpeg (picture, image_new, conf.width, conf.height, conf.quality);
			fclose(picture);
			snapshot=0;
		}
		fflush(0);
	}
	perror("Somebody stole the video device, lets hope we got his picture");
	close (dev);
	free(image_ref);
	free(image_out);
	exit(0);
}
