/* Copyright 1999, 2000 Red Hat, Inc.
 *
 * This software may be freely redistributed under the terms of the GNU
 * public license.
 *
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

#include <ctype.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "usb.h"
#include "modules.h"

static void usbFreeDevice(struct usbDevice *dev)
{
	free(dev->productrevision);
	freeDevice((struct device *) dev);
}

static void usbWriteDevice(FILE * file, struct usbDevice *dev)
{
	writeDevice(file, (struct device *) dev);
	fprintf(file, "usbclass: %d\nusbsubclass: %d\nusbprotocol: %d\n",
		dev->usbclass, dev->usbsubclass, dev->usbprotocol);
	fprintf(file, "usbbus: %d\nusblevel: %d\nusbport: %d\n",
		dev->usbbus, dev->usblevel, dev->usbport);
	fprintf(file, "vendorId: %04x\ndeviceId: %04x\n",
		dev->vendorId, dev->deviceId);
	if (dev->productrevision)
		fprintf(file, "productrevision: %s\n",
			dev->productrevision);
}

static int usbCompareDevice(struct usbDevice *dev1, struct usbDevice *dev2)
{
	int x;

	if (!dev1 || !dev2)
		return 1;
	x = compareDevice((struct device *) dev1, (struct device *) dev2);
	if (x && x != 2)
		return x;
	if ((dev1->vendorId == dev2->vendorId) &&
	    (dev1->deviceId == dev2->deviceId)) {
		if (dev1->productrevision == NULL
		    && dev2->productrevision == NULL)
			return 0;
		if (dev1->productrevision == NULL
		    || dev2->productrevision == NULL)
			return 1;
		if (strcmp(dev1->productrevision, dev2->productrevision) ==
		    0)
			return 0;
	}
	return x;
}


struct usbDevice *usbNewDevice(struct usbDevice *old)
{
	struct usbDevice *ret;

	ret = malloc(sizeof(struct usbDevice));
	memset(ret, '\0', sizeof(struct usbDevice));
	ret =
	    (struct usbDevice *) newDevice((struct device *) old,
					   (struct device *) ret);
	ret->bus = BUS_USB;
	ret->newDevice = usbNewDevice;
	ret->freeDevice = usbFreeDevice;
	ret->writeDevice = usbWriteDevice;
	ret->compareDevice = usbCompareDevice;
	if (old && old->bus == BUS_USB) {
		ret->usbclass = old->usbclass;
		ret->usbsubclass = old->usbsubclass;
		ret->usbprotocol = old->usbprotocol;
		ret->usbbus = old->usbbus;
		ret->usblevel = old->usblevel;
		ret->usbport = old->usbport;
		ret->vendorId = old->vendorId;
		ret->deviceId = old->deviceId;
		if (old->productrevision)
			ret->productrevision =
			    strdup(old->productrevision);
	}
	return ret;
}

struct usbdesc {
	unsigned int vendorId;
	unsigned int deviceId;
	char *desc;
};

static struct usbdesc *usbDeviceList = NULL;
static int numUsbDevices = 0;

static int devCmp(const void *a, const void *b)
{
	const struct usbdesc *one = a;
	const struct usbdesc *two = b;
	int x, y;

	x = one->vendorId - two->vendorId;
	y = one->deviceId - two->deviceId;
	if (x)
		return x;
	return y;
}

int usbReadDrivers(char *filename)
{
	int fd;
	char *b, *buf, *tmp, *ptr;
	unsigned int vend, dev;
	struct usbdesc tmpdev;
	char *vendor;

	if (filename) {
		fd = open(filename, O_RDONLY);
		if (fd < 0)
			return -1;
	} else {
		fd = open("/usr/share/kudzu/usb.ids", O_RDONLY);
		if (fd < 0) {
			fd = open("./usb.ids", O_RDONLY);
			if (fd < 0)
				return -1;
		}
	}
	b = buf = bufFromFd(fd);

	while (*buf) {
		ptr = buf;
		while (*ptr && *ptr != '\n')
			ptr++;
		if (*ptr) {
			*ptr = '\0';
			ptr++;
		}
		if (*buf == '#') {
			buf = ptr;
			continue;
		}
		if (isalnum(*buf)) {
			tmp = buf;
			while (*tmp && !isspace(*tmp))
				tmp++;
			if (*tmp) {
				*tmp = '\0';
				do
					tmp++;
				while (isspace(*tmp));
			}
			vend = strtol(buf, NULL, 16);
			vendor = tmp;
		}
		if (*buf == '\t') {
			buf++;
			tmp = buf;
			while (*tmp && !isspace(*tmp))
				tmp++;
			if (*tmp) {
				*tmp = '\0';
				do
					tmp++;
				while (isspace(*tmp));
			}
			dev = strtol(buf, NULL, 16);
			tmpdev.vendorId = vend;
			tmpdev.deviceId = dev;
			tmpdev.desc =
			    malloc(strlen(tmp) + 2 + strlen(vendor));
			snprintf(tmpdev.desc,
				 strlen(tmp) + 2 + strlen(vendor), "%s %s",
				 vendor, tmp);
			usbDeviceList =
			    realloc(usbDeviceList,
				    (numUsbDevices +
				     1) * sizeof(struct usbdesc));
			usbDeviceList[numUsbDevices] = tmpdev;
			numUsbDevices++;
		}
		buf = ptr;
	}
	free(b);
	qsort(usbDeviceList, numUsbDevices, sizeof(struct usbdesc),
	      devCmp);
	return 0;
}

static void parseTopologyLine(char *line, struct usbDevice *usbdev)
{
	usbdev->usbbus = atoi(&line[8]);
	usbdev->usblevel = atoi(&line[15]);
	usbdev->usbport = atoi(&line[31]);
}

static enum deviceClass usbToKudzu(int usbclass, int usbsubclass, int usbprotocol)
{
	switch (usbclass) {
	case 1:
		return CLASS_AUDIO;
	case 2:
		switch (usbsubclass) {
		case 2:
			return CLASS_MODEM;
		case 6:
		case 7:
			return CLASS_NETWORK;
		default:
			return CLASS_OTHER;
		}
	case 3:
		switch (usbprotocol) {
		case 1:
			return CLASS_KEYBOARD;
		case 2:
			return CLASS_MOUSE;
		default:
			return CLASS_OTHER;
		}

	case 7:
		return CLASS_PRINTER;
	case 8:
		switch (usbsubclass) {
		case 4:
			return CLASS_FLOPPY;
		case 6:
			return CLASS_HD;
		}
	default:
		return CLASS_OTHER;
	}
}


static void parseDescriptorLine(char *line, struct usbDevice *usbdev)
{
	if (!
	    (usbdev->usbclass || usbdev->usbsubclass
	     || usbdev->usbprotocol)) {
		usbdev->usbclass = atoi(&line[30]);
		usbdev->usbsubclass = atoi(&line[44]);
		usbdev->usbprotocol = atoi(&line[52]);
		usbdev->class =
		    usbToKudzu(usbdev->usbclass, usbdev->usbsubclass,
			       usbdev->usbprotocol);
		if (usbdev->class == CLASS_MOUSE) {
			free(usbdev->desc);
			free(usbdev->driver);
			if (usbdev->device)
				free(usbdev->device);
			usbdev->desc = strdup("Generic USB Mouse");
			usbdev->driver = strdup("mousedev");
			usbdev->device = strdup("input/mice");
		}
		if (usbdev->class == CLASS_KEYBOARD) {
			free(usbdev->desc);
			free(usbdev->driver);
			usbdev->desc = strdup("Generic USB Keyboard");
			usbdev->driver = strdup("keybdev");
			usbdev->class = CLASS_KEYBOARD;
		}
		if (usbdev->class == CLASS_FLOPPY ||
		    usbdev->class == CLASS_CDROM ||
		    usbdev->class == CLASS_HD) {
			free(usbdev->driver);
			usbdev->driver = strdup("usb-storage");
		}
		if (usbdev->class == CLASS_AUDIO) {
			free(usbdev->driver);
			usbdev->driver = strdup("audio");
		}
	}
}

static void parseIdLine(char *line, struct usbDevice *usbdev)
{
	usbdev->vendorId = strtol(&line[11], NULL, 16);
	usbdev->deviceId = strtol(&line[23], NULL, 16);
}

static void parseStringDescriptorLine(char *line, struct usbDevice *usbdev)
{
	int x;
	char *tmp;

	if ((tmp = strcasestr(line, "product")) != NULL) {
		if (usbdev->desc)
			free(usbdev->desc);
		usbdev->desc = strdup(tmp + 8);
		for (x = 0; usbdev->desc[x]; x++)
			if (usbdev->desc[x] == '\n')
				usbdev->desc[x] = '\0';
	}
}

void usbFreeDrivers()
{
	int x;
	if (usbDeviceList) {
		for (x = 0; x < numUsbDevices; x++) {
			free(usbDeviceList[x].desc);
		}
	}
	free(usbDeviceList);
	usbDeviceList = NULL;
	numUsbDevices = 0;
}

struct device *usbProbe(enum deviceClass probeClass, int probeFlags,
			struct device *devlist)
{
	FILE *usbdevicelist;
	char line[255];
	struct usbDevice *usbdev = NULL;
	struct usbdesc *searchDev, key;
	struct confModules *cf;
	struct module *probeMods = NULL;
	int numMods, i, init_list = 0;
	char *alias = NULL;

	if (
	    (probeClass == CLASS_UNSPEC) ||
	    (probeClass == CLASS_OTHER) ||
	    (probeClass == CLASS_CDROM) ||
	    (probeClass == CLASS_HD) ||
	    (probeClass == CLASS_FLOPPY) ||
	    (probeClass == CLASS_KEYBOARD) ||
	    (probeClass == CLASS_MOUSE) ||
	    (probeClass == CLASS_AUDIO) ||
	    (probeClass == CLASS_MODEM) ||
	    (probeClass == CLASS_NETWORK)
	    ) {
		
		if (!usbDeviceList) {
			usbReadDrivers(NULL);
			init_list = 1;
		}
		probeMods = malloc(2 * sizeof(struct module));
		probeMods[0].name = NULL;
		cf = readConfModules("/etc/modules.conf");
		if (cf && (alias = getAlias(cf, "usb-controller"))
		    && !loadModule(alias)) {
			probeMods[0].name = strdup(alias);
			probeMods[0].loaded = 1;
			probeMods[1].name = NULL;
			free(alias);
			numMods = 1;
			for (i = 1;; i++) {
				snprintf(line, 80, "usb-controller%d", i);
				if ((alias = getAlias(cf, line))
				    && !loadModule(alias)) {
					probeMods[numMods].name =
					    strdup(alias);
					free(alias);
					probeMods[numMods].loaded = 1;
					probeMods[numMods + 1].name = NULL;
					numMods++;
				} else {
					break;
				}
			}
		}
		if (alias)
		  free(alias);
		if (cf)
		  freeConfModules(cf);

		usbdevicelist = fopen("/proc/bus/usb/devices", "r");
		if (usbdevicelist == NULL)
			goto out;
		while (fgets(line, 255, usbdevicelist)) {	/* device info */
			switch (line[0]) {
			case 'T':
				if (usbdev != NULL) {
					key.vendorId = usbdev->vendorId;
					key.deviceId = usbdev->deviceId;
					searchDev =
					    bsearch(&key, usbDeviceList,
						    numUsbDevices,
						    sizeof(struct usbdesc),
						    devCmp);
					if (searchDev) {
						free(usbdev->desc);
						usbdev->desc =
						    strdup(searchDev->
							   desc);
					}
					if (usbdev->class == probeClass ||
					    probeClass == CLASS_UNSPEC) {
						usbdev->next = devlist;
						devlist =
						    (struct device *)
						    usbdev;
					} else {
						usbFreeDevice(usbdev);
					}
				}
				usbdev = usbNewDevice(NULL);
				usbdev->desc = strdup("unknown");
				usbdev->driver = strdup("unknown");
				usbdev->productrevision =
				    strdup("unknown");
				parseTopologyLine(line, usbdev);
				break;
			case 'I':
				parseDescriptorLine(line, usbdev);
				break;
			case 'P':
				parseIdLine(line, usbdev);
			case 'S':
				parseStringDescriptorLine(line, usbdev);
				break;
			default:
				break;
			}
		}
		if (usbdev != NULL) {
			key.vendorId = usbdev->vendorId;
			key.deviceId = usbdev->deviceId;
			searchDev =
			    bsearch(&key, usbDeviceList, numUsbDevices,
				    sizeof(struct usbdesc), devCmp);
			if (searchDev) {
				free(usbdev->desc);
				usbdev->desc = strdup(searchDev->desc);
			}
			if (usbdev->class == probeClass ||
			    probeClass == CLASS_UNSPEC) {
				usbdev->next = devlist;
				devlist = (struct device *) usbdev;
			} else {
				usbFreeDevice(usbdev);
			}
		}
		fclose(usbdevicelist);
	      out:

		if (probeMods) {
			for (i = 0; probeMods[i].name; i++) {
				if (!removeModule(probeMods[i].name)) {
					probeMods[i].loaded = 0;
					free(probeMods[i].name);
				}
			}
			free(probeMods);
		}
	}
	if (usbDeviceList && init_list)
	  usbFreeDrivers();
	return devlist;
}
