/*
 * 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; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 *
 * Copyright (C) 2003 Andreas Ehliar <ehliar@lysator.liu.se>
 * 
 */


/* This code is ugly */


#include <stdlib.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <asm/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/hiddev.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>


#define CTRL_READ 1
#define CTRL_WRITE 2
#define CTRL_CONTIGUOUS 4 /* No real support for changing non contiguous values right now */
#define CTRL_CHANGE 8

struct control_info {
	unsigned usage_code; 
	char *name;
	unsigned flags;
};

struct ctrl_list {
	struct ctrl_list *next;
	unsigned report_type;
	unsigned report_id;
	unsigned field_index;
	unsigned usage_index;
	char *name;
	unsigned usage_code;
	__s32 value;
	unsigned flags;
	unsigned num_fields;
};

/* No fancy structure here */
static struct control_info c_info[] = {
	{0x820010, "Brightness", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820012, "Contrast", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820016, "Red Video Gain", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820018, "Green Video Gain", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x82001A, "Blue Video Gain", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x82001C, "Focus", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820020, "Horizontal Position", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820022, "Horizontal Size", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820024, "Horizontal Pincushion", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820026, "Horizontal Pincushion Balance", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820028, "Horizontal Misconvergence", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x82002A, "Horizontal Linearity", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x82002C, "Horizontal Linear Balance", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820030, "Vertical Position", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820032, "Vertical Size", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820034, "Vertical Pincushion", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820036, "Vertical Pincushion Balance", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820038, "Vertical Misconvergence", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x82003A, "Vertical Linearity", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x82003C, "Vertical Linearity Balance", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820040, "Parallelogram Distortion (Key Balance)", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820042, "Trapezoidal Distortion (Key)", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820044, "Tilt (Rotation)", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820046, "Top Corner Distortion Control", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820048, "Top Corner Distortion Balance", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x82004A, "Bottom Corner Distortion Control", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x82004C, "Bottom Corner Distortion Balance", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820056, "Horizontal Moire", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820058, "Vertical Moire", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x82006c, "Red Video Black Level", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x82006e, "Green Video Black Level", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	{0x820070, "Blue Video Black Level", CTRL_READ|CTRL_WRITE|CTRL_CONTIGUOUS},
	/* Non-contiguous Controls read/write*/
	{0x82005e, "Input level select", CTRL_READ|CTRL_WRITE},
	{0x820060, "Input source select", CTRL_READ|CTRL_WRITE},
	{0x8200ca, "On Screen Display", CTRL_READ|CTRL_WRITE},
	{0x8200d4, "StereoMode", CTRL_READ|CTRL_WRITE},
	/* Non-contiguous Controls read only*/
	{0x8200a2, "Auto Size Center", CTRL_READ},
	{0x8200a4, "Polarity Horizontal Synchronization", CTRL_READ},
	{0x8200a6, "Polarity Vertical Synchronization", CTRL_READ},
	{0x8200a8, "Synchronization Type", CTRL_READ},
	{0x8200aa, "Screen Orientation", CTRL_READ},
	{0x8200ac, "Horizontal Frequency", CTRL_READ},
	{0x8200ae, "Vertical Frequency", CTRL_READ},
	/* Non-contiguous Controls write only*/
	{0x820001, "Degauss", CTRL_WRITE}, /* I would recommend against this setting in a procmail rule */
	{0x8200b0, "Settings", CTRL_WRITE},
	{0x0, NULL, 0}
};

/* Find a control associated with a certain usage code */
struct control_info *findcontrol(int usage_code)
{
	int i;
	i=0;
	while(c_info[i].name != NULL){
		if(c_info[i].usage_code == usage_code){
			return &c_info[i];
		}
		i++;
	}
	return NULL;
}


/* Check if fd corresponds to a USB HIDDEV Monitor device */
int find_monitor(int fd,int verbose)
{
	struct hiddev_devinfo device_info;
	int i;
	int appl;
	int found=0;

	ioctl(fd, HIDIOCGDEVINFO, &device_info);

	for (i = 0; i < device_info.num_applications; i++) {
		appl = ioctl(fd, HIDIOCAPPLICATION, i);
		if (appl > 0) {
			if(verbose){
				fprintf(stderr,"Found application 0x%x\n",appl);
			}
			/* The magic values come from various usage table specs */
			if((appl >> 16) == 0x80){
				found = 1;
			}
		}else{
		  perror("ioctl");
		  exit(1);
		}
	}
	return found;
  
}


/* Find all controls for this monitor and return it in a linked list */
struct ctrl_list *scanmonitor(int fd,int verbose)
{
	struct ctrl_list *head;

	int i,j,ret;
	struct hiddev_report_info rinfo;
	struct hiddev_field_info finfo;
	struct hiddev_usage_ref uref;
	


	head = NULL;


	if(verbose){
		fprintf(stderr,"Fetching data from device\n");
	}

	if(ioctl(fd,HIDIOCINITREPORT,0) < 0){
		perror("ioctl");
		exit(1);
	}

	rinfo.report_type = HID_REPORT_TYPE_FEATURE;
	rinfo.report_id = HID_REPORT_ID_FIRST;
	ret = ioctl(fd, HIDIOCGREPORTINFO, &rinfo);

	/* I'm ashamed to admit it, but I don't understand some of this */
	while (ret >= 0) {
		for (i = 0; i < rinfo.num_fields; i++) { 
			finfo.report_type = rinfo.report_type;
			finfo.report_id = rinfo.report_id;
			finfo.field_index = i;

			ioctl(fd, HIDIOCGFIELDINFO, &finfo);

			for (j = 0; j < finfo.maxusage; j++) {
				struct control_info *ctrl;
				uref.report_type = rinfo.report_type;
				uref.report_id = rinfo.report_id;
				uref.field_index = i;
				uref.usage_index = j;
				
				ioctl(fd, HIDIOCGUCODE, &uref);
				ioctl(fd, HIDIOCGUSAGE, &uref);

				if(verbose){
					fprintf(stderr,"Usage code: 0x%x:  Value 0x%x\n",uref.usage_code,uref.value);
				}

				ctrl = findcontrol(uref.usage_code);
				if(ctrl){

					struct ctrl_list *newctrl;

					newctrl = malloc(sizeof(struct ctrl_list));
					if(!newctrl){
						perror("malloc");
						exit(1);
					}

					newctrl->next = head;
					newctrl->report_type = rinfo.report_type;
					newctrl->report_id = rinfo.report_id;
					newctrl->field_index = uref.field_index;
					newctrl->usage_index = uref.usage_index;
					newctrl->name = ctrl->name;
					newctrl->usage_code = uref.usage_code;
					newctrl->value = uref.value;
					newctrl->num_fields = rinfo.num_fields;
					newctrl->flags = ctrl->flags;
					head = newctrl;

				}else if((uref.usage_code >> 16) == 0x82){
					fprintf(stderr,"Warning: Unknown monitor control: 0x%x\n",uref.usage_code);
				}/* else, ignore */
				
			}
		}
		rinfo.report_id |= HID_REPORT_ID_NEXT;
		ret = ioctl(fd, HIDIOCGREPORTINFO, &rinfo);
	}
	
	return head;
}

/* Add a setting to ctrl with the name and value specified */
void add_setting(struct ctrl_list *ctrl, char *name, char *value)
{
	while(ctrl){
		if(!strcmp(name,ctrl->name)){
			if(ctrl->flags & CTRL_WRITE){
				if(ctrl->flags & CTRL_CHANGE){
					fprintf(stderr,"Value %s already set\n",name);
					exit(1);
				}
				/* Ok to set this now */
				ctrl->flags |= CTRL_CHANGE;
				ctrl->value = strtol(value,NULL,0);
				return;
			}else{
				fprintf(stderr,"Trying to set read only value %s\n", name);
				exit(1);
			}
		}
		ctrl = ctrl->next;
	}
	fprintf(stderr,"This monitor does not allow %s to be changed\n",name);
	exit(1);
}

/* Read settings from a file and add all to the ctrl list */
int read_settings(FILE *fp, struct ctrl_list *ctrl, int verbose)
{
	char line[160]; /* Should be enough, otherwise panic */
	int lineno = 0;

	fgets(line,158,fp);

	if(strcmp(line,"[USB Monitor Settings]\n")){
		fprintf(stderr,"Not USB Monitor Settings file\n");
		exit(1);
	}

	while(fgets(line,158,fp) != NULL){
		int i;
		char *name;
		char *value;
		enum state { FOUNDNONE, FOUNDNAME, FINDVALUE, FOUNDALL };
		enum state current;
		current = FOUNDNONE;

		i = 0;
		name = line;
		value = NULL;
		lineno++;

		for(i=0;i<158;i++){
			/* Parse a config line. This code is ugly...
			 * For some reason I always write ugly text parsing code in C */
			if(line[i] == '\0'){
				break; /* Get out of for */
			}else if(line[i] == ';'){
				line[i] = '\0';
				if(current == FOUNDNONE){
					break; /* Get out of for */
				}else if(current == FOUNDALL){
					break; /* Get out of for */
				}else{
					fprintf(stderr,"Line %d is malformed\n",lineno);
					exit(1);
				}
					
			}else if(line[i] == ':'){
				if(current != FOUNDNAME){
					fprintf(stderr,"Line %d is malformed\n",lineno);
					exit(1);
				}

				line[i] = '\0';
				if(i+1 > 158){
					fprintf(stderr,"Line %d too long\n", lineno);
					exit(1);
				}
				value = &line[i+1];
				current = FINDVALUE;
			}else if(isspace(line[i])){
				if(current == FOUNDNONE){
					name = &line[i];
				}
			}else{
				if(current == FINDVALUE){
					/* Should do a more exaustive test here */
					if(isdigit(line[i])){
						current = FOUNDALL;
						value = &line[i];
					}else{
						fprintf(stderr,"Not a value at line %d\n",lineno);
						exit(1);
					}
				}else if(current == FOUNDNONE){
					current = FOUNDNAME;
				}
			}
		}

		switch(current){
		case FOUNDNONE:
			break; /* Nothing on this line, just a comment? */
		case FOUNDNAME:
		case FINDVALUE:
			fprintf(stderr,"Line %d is malformed\n",lineno);
			exit(1);
		case FOUNDALL:
			add_setting(ctrl, name, value);
		}


	}

	return 0;

		
	
}


/* Apply changes to ctrl to the monitor */
void set_settings(int fd,struct ctrl_list *ctrl,int verbose)
{
	struct hiddev_report_info rinfo;
	struct hiddev_usage_ref uref;
	while(ctrl){
		if(ctrl->flags & CTRL_CHANGE){
			if(verbose){
				fprintf(stderr,"Setting usage code 0x%x (%s) to 0x%x\n", ctrl->usage_code, ctrl->name, ctrl->value);
			}
			uref.report_type = ctrl->report_type;
			uref.report_id = ctrl->report_id;
			uref.field_index = ctrl->field_index;
			uref.usage_index = ctrl->usage_index;
			uref.usage_code = ctrl->usage_code;
			uref.value = ctrl->value;
			ioctl(fd,HIDIOCSUSAGE,&uref);
			rinfo.report_type = ctrl->report_type;
			rinfo.report_id = ctrl->report_id;
			rinfo.num_fields = ctrl->num_fields;
			ioctl(fd,HIDIOCSREPORT,&rinfo);
		}
		ctrl = ctrl->next;
	}
}


int main (int argc, char **argv) {

	int fd;
	int c;
	char *devname = "/dev/usb/hiddev0";
	int verbose = 0;
	int setcontrols = 0;
	int printcontrols = 1;
	char *configname = NULL;
	
	struct ctrl_list *controls;

	while ((c = getopt(argc,argv,"vhs:f:")) != EOF) {
		switch(c){
		case 'v':
			verbose = 1;
			break;
		case 's':
			setcontrols = 1;
			printcontrols = 0;
			configname = optarg;
			break;
		case 'h':
		case '?':
			fprintf(stderr, "Usage: %s [-vh] [-s <configfile>] [-f <usb device node>] [filename]\n\n",argv[0]);
			fprintf(stderr, "setusbmonitor v0.1\n");
			fprintf(stderr, " -v\t\tVerbose\n");
			fprintf(stderr, " -s\t\tRead settings from stdin\n");
			fprintf(stderr, " -f\t\tSpecify usb device node (%s default)\n",devname);
			fprintf(stderr, " -h\t\tView usage\n");
			exit(1);
		case 'f':
			devname = optarg;
			break;
		}

	}

	if(optind < argc){
		fprintf(stderr,"Too many arguments\n");
		exit(1);
	}

	if ((fd = open(devname, O_RDONLY)) < 0) {
		fprintf(stderr,"Cannot open %s: %s.\n",devname,strerror(errno));
		exit(1);
	}

	if(!find_monitor(fd,verbose)){
	  fprintf(stderr,"I do not recognize this as a monitor\n");
	  exit(1);
	}

	controls = scanmonitor(fd,verbose);

	if(!controls){
		fprintf(stderr,"No controls found\n");
		exit(1);
	}

	if(setcontrols){
		if(configname){
			FILE *configfp;
			configfp = fopen(configname,"r");
			if(!configfp){
				perror("fopen");
				exit(1);
			}
			read_settings(configfp,controls,verbose);
		}else{
			read_settings(stdin,controls,verbose);
		}

		set_settings(fd,controls,verbose);
		exit(0);
	}

	/* A bit messy printout for now */
	if(printcontrols){
		printf("[USB Monitor Settings]\n");
		printf("\n");
		while(controls){
			char *contig;
			if(controls->flags & CTRL_CONTIGUOUS){
				contig = "";
			}else{
				contig = "(Non contiguous)";
			}
			if((controls->flags & (CTRL_READ|CTRL_WRITE)) == (CTRL_READ|CTRL_WRITE)){
				printf("%s:\t0x%x\t; read/write%s\n",controls->name,controls->value,contig);
			}else if(controls->flags & CTRL_READ){
				printf(";%s:\t0x%x\t; read only%s\n",controls->name, controls->value,contig);
			}else if(controls->flags & CTRL_WRITE){
				printf(";%s:\t???\t; write only%s\n",controls->name,contig);
			}
			controls = controls->next;
		}
		exit(0);
	}
	
	exit(0);
}
