/*
 * Guillaume Cottenceau (gc)
 *
 * Copyright 2000 Mandriva
 *
 * 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.
 *
 */

/*
 * Portions from Erik Troan (ewt@redhat.com)
 *
 * Copyright 1996 Red Hat Software 
 *
 */


/*
 * This contains stuff related to probing:
 * (1) any (actually only SCSI, NET, CPQ, USB Controllers) devices (autoprobe for PCI and USB)
 * (2) IDE media
 * (3) SCSI media
 * (4) ETH devices
 */


#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <sys/socket.h>
#include <net/if.h>
#include <linux/sockios.h>
#include <linux/ethtool.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <pci/pci.h>
#include <libldetect.h>
#include <errno.h>
#include <glob.h>
#include "stage1.h"

#include "log.h"
#include "utils.h"
#include "frontend.h"
#include "modules.h"
#include "pci-ids.h"
#ifdef ENABLE_USB
#include "usb-ids.h"
#endif
#ifdef ENABLE_PCMCIA
#include "sysfs/libsysfs.h"
#include "pcmcia-ids.h"
#endif

#include "probing.h"


struct media_info {
	char * name;
	char * model;
	enum media_type type;
};


static void warning_insmod_failed(enum insmod_return r)
{
	if (IS_AUTOMATIC && r == INSMOD_FAILED_FILE_NOT_FOUND)
		return;
	if (r != INSMOD_OK) {
		if (r == INSMOD_FAILED_FILE_NOT_FOUND)
			stg1_error_message("This floppy doesn't contain the driver.");
		else
			stg1_error_message("Warning, installation of driver failed. (please include msg from <Alt-F3> for bugreports)");
	}
}

#ifndef DISABLE_NETWORK
const char * safe_descr(const char * text) {
	return text ? text : "unknown";
}

char * get_net_intf_description(char * intf_name)
{
	struct ifreq ifr;
	struct ethtool_drvinfo drvinfo;
	int s = socket(AF_INET, SOCK_DGRAM, 0);
	char *res;

	memset(&ifr, 0, sizeof(ifr));
	strncpy(ifr.ifr_name, intf_name, IFNAMSIZ);

	drvinfo.cmd = ETHTOOL_GDRVINFO;
	ifr.ifr_data = (caddr_t) &drvinfo;

	if (ioctl(s, SIOCETHTOOL, &ifr) != -1) {
		res = drvinfo.driver;
	} else {
		perror("SIOCETHTOOL");
		res = "unknown";
	}
	close(s);
	return strdup(res);
}
#endif

static int device_match_modules_list(struct pciusb_entry *e, char **modules, unsigned int modules_len) {
	int i;
	if (!e->module)
		return 0;
	for (i = 0; i < modules_len; i++)
		if (!strcmp(modules[i], e->module))
			return 1;
	return 0;
}

struct pcitable_entry *detected_devices = NULL;
int detected_devices_len = 0;

static void detected_devices_destroy(void)
{
	if (detected_devices)
		free(detected_devices);
}

static struct pcitable_entry *detected_device_new(void)
{
	static int detected_devices_maxlen = 0;
	if (detected_devices_len >= detected_devices_maxlen) {
		detected_devices_maxlen += 32;
		if (detected_devices == NULL)
 			detected_devices = malloc(detected_devices_maxlen * sizeof(*detected_devices));
		else
			detected_devices = realloc(detected_devices, detected_devices_maxlen * sizeof(*detected_devices));
		if (detected_devices == NULL)
			log_perror("detected_device_new: could not (re)allocate table. Let it crash, sorry");
	}
	return &detected_devices[detected_devices_len++];
}

/* FIXME: factorize with probe_that_type() */

static void add_detected_device(unsigned short vendor, unsigned short device, unsigned int subvendor, unsigned int subdevice, const char *name, const char *module)
{
	struct pcitable_entry *dev = detected_device_new();
	dev->vendor = vendor;
	dev->device = device;
	dev->subvendor = subvendor;
	dev->subdevice = subdevice;
	strncpy(dev->module, module, sizeof(dev->module) - 1);
	dev->module[sizeof(dev->module) - 1] = '\0';
	strncpy(dev->description, safe_descr(name), sizeof(dev->description) - 1);
	dev->description[sizeof(dev->description) - 1] = '\0';
	log_message("detected device (%04x, %04x, %04x, %04x, %s, %s)", vendor, device, subvendor, subdevice, name, module);
}

static int add_detected_device_if_match(struct pciusb_entry *e, char **modules, unsigned int modules_len)
{
	int ret = device_match_modules_list(e, modules, modules_len);
	if (ret)
		add_detected_device(e->vendor, e->device, e->subvendor, e->subdevice,
				    e->text, e->module);
	return ret;
}

void probing_detect_devices()
{
        static int already_detected_devices = 0;
	struct pciusb_entries entries;
	int i;

	if (already_detected_devices)
		return;

	entries = pci_probe();
	for (i = 0; i < entries.nb; i++) {
		struct pciusb_entry *e = &entries.entries[i];
#ifndef DISABLE_PCIADAPTERS
#ifndef DISABLE_MEDIAS
		if (add_detected_device_if_match(e, medias_ide_pci_modules, medias_ide_pci_modules_len))
			continue;
		if (add_detected_device_if_match(e, medias_other_pci_modules, medias_other_pci_modules_len))
			continue;
#endif

#ifndef DISABLE_NETWORK
		if (add_detected_device_if_match(e, network_pci_modules, network_pci_modules_len))
			continue;
#endif

#ifdef ENABLE_USB
		if (add_detected_device_if_match(e, usb_controller_modules, usb_controller_modules_len))
			continue;
#endif
#endif
		/* device can't be found in built-in pcitables, but keep it */
		add_detected_device(e->vendor, e->device, e->subvendor, e->subdevice, e->text, e->module);
	}
	pciusb_free(&entries);

	already_detected_devices = 1;
}

void probing_destroy(void)
{
	detected_devices_destroy();
}

#ifndef DISABLE_MEDIAS
static const char * get_alternate_module(const char * name)
{
	struct alternate_mapping {
		const char * a;
		const char * b;
	};
	static struct alternate_mapping mappings[] = {
                { "ahci", "ata_piix" },
        };
	int mappings_nb = sizeof(mappings) / sizeof(struct alternate_mapping);
        int i;

	for (i=0; i<mappings_nb; i++) {
		const char * alternate = NULL;
		if (streq(name, mappings[i].a))
			alternate = mappings[i].b;
		else if (streq(name, mappings[i].b))
			alternate = mappings[i].a;
		if (alternate) {
			log_message("found alternate module %s for driver %s", alternate, name);
			return alternate;
		}
	}
        return NULL;
}
#endif

void discovered_device(enum driver_type type, const char * description, const char * driver)
{
	description = safe_descr(description);

	enum insmod_return failed = INSMOD_FAILED;
#ifndef DISABLE_MEDIAS
	if (type == MEDIA_ADAPTERS) {
		const char * alternate = NULL;
		wait_message("Loading driver for media adapter:\n \n%s", description);
		failed = my_modprobe(driver, MEDIA_ADAPTERS, NULL);
		alternate = get_alternate_module(driver);
		if (!IS_NOAUTO && alternate) {
			failed = failed || my_modprobe(alternate, MEDIA_ADAPTERS, NULL);
		}
		remove_wait_message();
		warning_insmod_failed(failed);
	}
#endif
#ifndef DISABLE_NETWORK
	if (type == NETWORK_DEVICES) {
		log_message("NET: Loading driver for network device %s", description);
		wait_message("Loading driver for network device:\n \n%s", description);
		failed = my_modprobe(driver, NETWORK_DEVICES, NULL);
		warning_insmod_failed(failed);
		remove_wait_message();
	}
#endif
#ifdef ENABLE_USB
	if (type == USB_CONTROLLERS)
                /* we can't allow additional modules floppy since we need usbhid for keystrokes of usb keyboards */
		failed = my_modprobe(driver, USB_CONTROLLERS, NULL);
#endif
}

void probe_pci_modules(enum driver_type type, char **pci_modules, unsigned int  pci_modules_len) {
	struct pciusb_entries entries;
	int i;

	entries = pci_probe();
	for (i = 0; i < entries.nb; i++) {
		struct pciusb_entry *e = &entries.entries[i];
		if (device_match_modules_list(e, pci_modules, pci_modules_len)) {
			log_message("PCI: device %04x %04x %04x %04x is \"%s\", driver is %s",
				    e->vendor, e->device, e->subvendor, e->subdevice, safe_descr(e->text), e->module);
			discovered_device(type, e->text, e->module);
		}
	}
	pciusb_free(&entries);
}

/** Loads modules for known virtio devices
 *
 * virtio modules are not being loaded using the PCI probing mechanism
 * because pcitable.gz does not have IDs for these devices.
 *
 * The possible correct solution for it is to fix the script which
 * generates pcitable.gz to handle the virtio_device_id structure.
 */
void probe_virtio_modules(void)
{
	struct pciusb_entries entries;
	int i;
	char *name;
	char *options;
	int loaded_pci = 0;

	entries = pci_probe();
	for (i = 0; i < entries.nb; i++) {
		struct pciusb_entry *e = &entries.entries[i];
		if (e->vendor == VIRTIO_PCI_VENDOR) {
			if (!loaded_pci) {
				log_message("loading virtio-pci");
				my_modprobe("virtio_pci", ANY_DRIVER_TYPE, NULL);
				loaded_pci = 1;
			}

			name = NULL;
			options = NULL;

			switch (e->subdevice) {
			case VIRTIO_ID_NET:
				name = "virtio_net";
				options = "csum=0";
				break;
			case VIRTIO_ID_BLOCK:
				name = "virtio_blk";
				break;
			case VIRTIO_ID_BALLOON:
				name = "virtio_balloon";
				break;
			default:
				log_message("warning: unknown virtio device %04x", e->device);
			}
			if (name) {
				log_message("virtio: loading %s", name);
				my_modprobe(name, ANY_DRIVER_TYPE, options);
			}
		}
	}
	pciusb_free(&entries);
}

#ifdef ENABLE_USB
void probe_that_type(enum driver_type type, enum media_bus bus)
#else
void probe_that_type(enum driver_type type, enum media_bus bus __attribute__ ((unused)))
#endif
{
        static int already_probed_usb_controllers = 0;
        static int already_loaded_usb_scsi = 0;
        static int already_probed_virtio_devices = 0;

	/* ---- PCI probe ---------------------------------------------- */
	if (bus != BUS_USB) {
		switch (type) {
#ifndef DISABLE_PCIADAPTERS
#ifndef DISABLE_MEDIAS
			static int already_probed_media_adapters = 0;
		case MEDIA_ADAPTERS:
			if (already_probed_media_adapters)
				break;
			already_probed_media_adapters = 1;
			probe_pci_modules(type, medias_ide_pci_modules, medias_ide_pci_modules_len);
			probe_pci_modules(type, medias_other_pci_modules, medias_other_pci_modules_len);
			break;
#endif
#ifndef DISABLE_NETWORK
		case NETWORK_DEVICES:
			probe_pci_modules(type, network_pci_modules, network_pci_modules_len);
			break;
#endif
#endif
#ifdef ENABLE_USB
		case USB_CONTROLLERS:
			if (already_probed_usb_controllers || IS_NOAUTO)
				break;
			already_probed_usb_controllers = 1;
			probe_pci_modules(type, usb_controller_modules, usb_controller_modules_len);
			break;
#endif
		case VIRTIO_DEVICES:
			if (already_probed_virtio_devices)
				break;
			probe_virtio_modules();
			already_probed_virtio_devices = 1;
			break;
		default:
			break;
		}
	}


#ifdef ENABLE_USB
	/* ---- USB probe ---------------------------------------------- */
	if ((bus == BUS_USB || bus == BUS_ANY) && !(IS_NOAUTO)) {
		static int already_mounted_usbdev = 0;
		struct pciusb_entries entries;
		int i;

		if (!already_probed_usb_controllers)
			probe_that_type(USB_CONTROLLERS, BUS_ANY);

		if (!already_mounted_usbdev) {
			already_mounted_usbdev = 1;
			wait_message("Detecting USB devices.");
			sleep(4); /* sucking background work */
			my_modprobe("usbhid", ANY_DRIVER_TYPE, NULL);
			remove_wait_message();
		}

		if (type != NETWORK_DEVICES)
			goto end_usb_probe;

		entries = usb_probe();
		for (i = 0; i < entries.nb; i++) {
			struct pciusb_entry *e = &entries.entries[i];
			if (device_match_modules_list(e, usb_modules, usb_modules_len)) {
				log_message("USB: device %04x %04x is \"%s\" (%s)", e->vendor, e->device, safe_descr(e->text), e->module);
				discovered_device(type, e->text, e->module);
			}
		}
		pciusb_free(&entries);
	end_usb_probe:;
	}
#endif

#ifdef ENABLE_PCMCIA
	/* ---- PCMCIA probe ---------------------------------------------- */
	if ((bus == BUS_PCMCIA || bus == BUS_ANY) && !(IS_NOAUTO)) {
		struct pcmcia_alias * pcmciadb = NULL;
		unsigned int len = 0;
		char *base = "/sys/bus/pcmcia/devices";
		DIR *dir;
		struct dirent *dent;

		dir = opendir(base);
		if (dir == NULL)
			goto end_pcmcia_probe;

		switch (type) {
#ifndef DISABLE_MEDIAS
		case MEDIA_ADAPTERS:
			pcmciadb = medias_pcmcia_ids;
			len      = medias_pcmcia_num_ids;
			break;
#endif
#ifndef DISABLE_NETWORK
		case NETWORK_DEVICES:
			pcmciadb = network_pcmcia_ids;
			len      = network_pcmcia_num_ids;
			break;
#endif
		default:
			goto end_pcmcia_probe;
                }

                for (dent = readdir(dir); dent != NULL; dent = readdir(dir)) {
			struct sysfs_attribute *modalias_attr;
			char keyfile[300];
			int i, id;

			if (dent->d_name[0] == '.')
				continue;

			log_message("PCMCIA: device found %s", dent->d_name);

			snprintf(keyfile, sizeof(keyfile)-1, "%s/%s/modalias", base, dent->d_name);
			modalias_attr = sysfs_open_attribute(keyfile);
			if (!modalias_attr)
				continue;
			if (sysfs_read_attribute(modalias_attr) != 0 || !modalias_attr->value) {
				sysfs_close_attribute(modalias_attr);
				continue;
			}

			log_message("PCMCIA: device found %s", modalias_attr->value);

			for (i = 0; i < len; i++) {
				if (!fnmatch(pcmciadb[i].modalias, modalias_attr->value, 0)) {
					char product[256];

					log_message("PCMCIA: device found %s (%s)", pcmciadb[i].modalias, pcmciadb[i].module);
					strcpy(product, "");
					for (id = 1; id <= 4; id++) {
						struct sysfs_attribute *product_attr;
						snprintf(keyfile, sizeof(keyfile)-1, "%s/%s/prod_id%d", base, dent->d_name, id);
						product_attr = sysfs_open_attribute(keyfile);
						if (!product_attr)
							continue;
						if (sysfs_read_attribute(product_attr) || !product_attr->value) {
							sysfs_close_attribute(product_attr);
							continue;
						}
						snprintf(product + strlen(product), sizeof(product)-strlen(product)-1, "%s%s", product[0] ? " " : "", product_attr->value);
						if (product[strlen(product)-1] == '\n')
							product[strlen(product)-1] = '\0';
						sysfs_close_attribute(product_attr);
					}

					if (!product[0])
						strcpy(product, "PCMCIA device");

					log_message("PCMCIA: device found %s (%s)", product, pcmciadb[i].module);
					discovered_device(type, product, pcmciadb[i].module);
				}
			}

			sysfs_close_attribute(modalias_attr);
		}
	end_pcmcia_probe:;
		if (dir)
			closedir(dir);
	}
#endif

        /* be sure to load usb-storage after media adapters, so that they are in
           same order than reboot, so that naming is the same */
        if (type == MEDIA_ADAPTERS && (bus == BUS_USB || bus == BUS_SCSI || bus == BUS_ANY) &&
            already_probed_usb_controllers && !already_loaded_usb_scsi) {
                already_loaded_usb_scsi = 1;
                /* we can't allow additional modules floppy since we need usbkbd for keystrokes of usb keyboards */
                my_modprobe("usb_storage", MEDIA_ADAPTERS, NULL); 
                if (module_already_present("ieee1394"))
                        my_modprobe("sbp2", MEDIA_ADAPTERS, NULL);
                wait_message("Detecting USB mass-storage devices.");
#ifndef DEBUG
                sleep(10); /* sucking background work */
#endif
                remove_wait_message();
        }
}


static struct media_info * medias = NULL;

// Read a short string from a file and strips it, intended for sysfs attributes
static ssize_t read_attribute(char *path, char *buf) {
    ssize_t l = 0;
    int fd = open(path, O_RDONLY);
    buf[0] = '\0';
    if (fd == -1) {
        log_message("Failed to open %s for reading", path);
    } else {
        ssize_t n = read(fd, buf, 32);
        if (n == -1) {
            log_message("Couldn't read file (%s)", path);
        } else {
            // Strip whitespaces and newline
            for (int i = n-1; i >= 0; i--) {
                if (buf[i] == '\n' || buf[i] == ' ')
                    continue;
                l = i+1;
                break;
            }
            buf[l] = '\0';
        }
        close(fd);
    }
    log_message("Content of %s was %s", path, buf);
    return l;
}

void find_media(enum media_bus bus)
{
	char buf[5000];
	struct media_info tmp[50];
	int count = 0;

	if (medias)
		free(medias); /* that does not free the strings, by the way */

	log_message("looking for media adapters");
	probe_that_type(MEDIA_ADAPTERS, bus);

	/* ----------------------------------------------- */
	log_message("looking for DAC960");
	{
		FILE * f;
		if ((f = fopen("/tmp/syslog", "rb"))) {
			while (fgets(buf, sizeof(buf), f)) {
				char * start;
				if ((start = strstr(buf, "/dev/rd/"))) {
					char * end = strchr(start, ':');
					if (!end)
						log_message("Inconsistency in syslog, line:\n%s", buf);
					else {
						*end = '\0';
						tmp[count].name = strdup(start+5);
						tmp[count].type = DISK;
						start = end + 2;
						end = strchr(start, ',');
						if (end) {
							*end = '\0';
							tmp[count].model = strdup(start);
						} else
							tmp[count].model = "(unknown)";
						log_message("DAC960: found %s (%s)", tmp[count].name, tmp[count].model);
						count++;
					}
				}
			}
			fclose(f);
		}
	}
	/* ----------------------------------------------- */
	log_message("looking for other disks");
	{
		glob_t globbuf;
		// TODO: We should switch everything to here, and later switch to ignoring
		// some types of disks (ram, loop, ...) rather than a list to accept.
		glob("/sys/block/nvme*", 0, NULL, &globbuf);
		glob("/sys/block/vd*", GLOB_APPEND, NULL, &globbuf);
		glob("/sys/block/cciss*", GLOB_APPEND, NULL, &globbuf);
		glob("/sys/block/fd*", GLOB_APPEND, NULL, &globbuf);
		glob("/sys/block/sd*", GLOB_APPEND, NULL, &globbuf);
		glob("/sys/block/st*", GLOB_APPEND, NULL, &globbuf);
		glob("/sys/block/sr*", GLOB_APPEND, NULL, &globbuf);

		for (int i = 0; i < globbuf.gl_pathc; i++) {
			char *name, *pathend;
			char path[64];
			char model[64];
			int vendor_length = 0;

			strncpy(path, globbuf.gl_pathv[i], sizeof(path));
			name = strdup(path + 11);

			// Replace ! with /, for example for cciss!c0d0 devices
			char * c = name;
			while((c = strchr(c, '!')) != NULL) {
				*c = '/';
				c++;
			}

			pathend = path + strlen(path);

			// Check if this device had been handled by other code
			int exists = 0;
			for (int j = 0; j < count; j++) {
				if (!strcmp(name, tmp[j].name)) {
					exists = 1;
					break;
				}
			}
			if (exists) {
				free(name);
				continue;
			}

			strcpy(model, "Unknown Disk");

			strcpy(pathend, "/device/vendor");
			vendor_length = read_attribute(path, model);

			if (vendor_length) {
				strcat(model, " ");
				vendor_length++;
			}

			strcpy(pathend, "/device/model");
			read_attribute(path, model+vendor_length);

			strcpy(pathend, "/capability");
			read_attribute(path, buf);
			long caps = strtol(buf, NULL, 16);

			// GENHD_FL_UP (0x0010): indicated that the block device is “up” but the kernel has removed that info
			if (caps && 0) {
				log_message("Ignoring device %s (not up)", name);
				free(name);
				continue;
			}

			if (caps & 0x0400) {
				log_message("Ignoring device %s (hidden)", name);
				free(name);
				continue;
			}

			tmp[count].type = DISK;
			if (caps & 0x0008 || !strncmp(name, "sr", 2)) {
				tmp[count].type = CDROM;
			} else if (!strncmp(name, "fd", 2)) {
				tmp[count].type = FLOPPY;
			} else if (!strncmp(name, "st", 2)) {
				tmp[count].type = TAPE;
			}

			tmp[count].name = name;
			tmp[count].model = strdup(model);
			count++;
		}
		globfree(&globbuf);
	}

	/* ----------------------------------------------- */
	tmp[count].name = NULL;
	count++;

	medias = _memdup(tmp, sizeof(struct media_info) * count);
}


/* Finds by media */
void get_medias(enum media_type media, char *** names, char *** models, enum media_bus bus)
{
	struct media_info * m;
	char * tmp_names[50];
	char * tmp_models[50];
	int count;

	find_media(bus);

	m = medias;

	count = 0;
	while (m && m->name) {
		if (m->type == media) {
			tmp_names[count] = strdup(m->name);
			tmp_models[count++] = strdup(m->model);
		}
		m++;
	}
	tmp_names[count] = NULL;
	tmp_models[count++] = NULL;

	*names = _memdup(tmp_names, sizeof(char *) * count);
	*models = _memdup(tmp_models, sizeof(char *) * count);
}


#ifndef DISABLE_NETWORK
static int is_net_interface_blacklisted(char *intf)
{
	/* see detect_devicess::is_lan_interface() */
	char * blacklist[] = { "lo", "ippp", "isdn", "plip", "ppp", "wifi", "sit", NULL };
	char ** ptr = blacklist;

	while (ptr && *ptr) {
		if (!strncmp(intf, *ptr, strlen(*ptr)))
			return 1;
		ptr++;
	}

	return 0;
}

char ** get_net_devices(void)
{
	char * tmp[50];
	static int already_probed = 0;
	FILE * f;
	int i = 0;

	if (!already_probed) {
		already_probed = 1; /* cut off loop brought by: probe_that_type => my_modprobe => get_net_devices */
		probe_that_type(NETWORK_DEVICES, BUS_ANY);
	}

	/* use /proc/net/dev since SIOCGIFCONF doesn't work with some drivers (rt2500) */
	f = fopen("/proc/net/dev", "rb");
	if (f) {
		char line[128];

		/* skip the two first lines */
		fgets(line, sizeof(line), f);
		fgets(line, sizeof(line), f);

		while (1) {
			char *start, *end;
			if (!fgets(line, sizeof(line), f))
				break;
			start = line;
			while (*start == ' ')
				start++;
			end = strchr(start, ':');
			if (end)
				end[0] = '\0';
			if (!is_net_interface_blacklisted(start)) {
				log_message("found net interface %s", start);
				tmp[i++] = strdup(start);
			} else {
				log_message("found net interface %s, but blacklisted", start);
			}
		}

		fclose(f);
	} else {
		log_message("net: could not open devices file");
	}

	tmp[i++] = NULL;

	return _memdup(tmp, sizeof(char *) * i);

}
#endif /* DISABLE_NETWORK */
