/*
 * Copyright 2020-2021 Stefan Ott
 *
 * This file is part of warmluft.
 *
 * Warmluft 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 3 of the License, or
 * (at your option) any later version.
 *
 * Foobar 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 Foobar.  If not, see <https://www.gnu.org/licenses/>.
 */

#include <dirent.h>
#include <fcntl.h>
#include <libgen.h>
#include <regex.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <linux/limits.h>
#include <sys/types.h>

#include "hwmon.h"

#define HWMON_BASE_PATH "/sys/class/hwmon"
#define HWMON_SENSOR_REGEX "^temp[0-9]+_input$"

#ifndef UNITTEST
static
#endif
int
is_temp_input(const char *name)
{
	regex_t re;
	int res;
	char msg[256];

	res = regcomp(&re, HWMON_SENSOR_REGEX, REG_EXTENDED);
	if (res != 0)
	{
		regerror(res, &re, msg, sizeof(msg));
		fprintf(stderr, "%s: %s\n", HWMON_SENSOR_REGEX, msg);
		return 0;
	}

	res = regexec(&re, name, 0, NULL, 0);

	regfree(&re);
	return (res == 0);
}

#ifndef UNITTEST
static
#endif
int
sensor_field_name(struct hwmon_sensor *s, const char *fn, char *buf, size_t len)
{
	char buf_file[PATH_MAX], *ptr, *file;

	snprintf(buf_file, sizeof(buf_file), "%s", s->path_in);
	file = basename(buf_file);

	ptr = strstr(file, "_input");
	if (!ptr)
	{
		return 1;
	}
	sprintf(ptr, fn);

	snprintf(buf, len, "%s", file);

	return 0;
}

static void
strip(char *in)
{
	char *nl;

	nl = index(in, '\n');
	if (nl)
	{
		*nl = 0;
	}
}

static int
load_device_name(struct hwmon_device *dev)
{
	int fd;
	char path[PATH_MAX + 5];

	snprintf(path, sizeof(path), "%s/%s", dev->path, "name");
	fd = open(path, O_RDONLY);
	if (fd == -1)
	{
		perror(path);
		return 1;
	}

	if (read(fd, dev->name, sizeof(dev->name)) == -1)
	{
		perror(path);
		close(fd);
		return 1;
	}

	strip(dev->name);

	close(fd);
	return 0;
}

static int
load_sensor_max(struct hwmon_device *dev, struct hwmon_sensor *sensor)
{
	char path[PATH_MAX], buf[NAME_MAX];
	int fd;

	if (sensor_field_name(sensor, "_max", buf, sizeof(buf)) != 0)
	{
		return 1;
	}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-truncation"
	snprintf(path, sizeof(path), "%s/%s", dev->path, buf);
#pragma GCC diagnostic pop

	fd = open(path, O_RDONLY);
	if (fd != -1)
	{
		if (read(fd, buf, sizeof(buf)) == -1)
		{
			perror("read");
			close(fd);
			return 1;
		}
		close(fd);

		sensor->temp_max = atof(buf) / 1000;
	}

	return 0;
}

static int
load_sensor_label(struct hwmon_device *dev, struct hwmon_sensor *sensor)
{
	char path[PATH_MAX], buf[NAME_MAX];
	int fd;

	if (sensor_field_name(sensor, "_label", buf, sizeof(buf)) != 0)
	{
		return 1;
	}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-truncation"
	snprintf(path, sizeof(path), "%s/%s", dev->path, buf);
#pragma GCC diagnostic pop

	fd = open(path, O_RDONLY);
	if (fd != -1)
	{
		if (read(fd, sensor->label, sizeof(sensor->label)) == -1)
		{
			perror("read");
			close(fd);
			return 1;
		}
		close(fd);
		strip(sensor->label);
	}

	return 0;
}

static int
load_device_sensors(DIR *dir, struct hwmon_device *dev)
{
	struct dirent *de;
	struct hwmon_sensor *row, **next;

	next = &dev->sensors;

	while ((de = readdir(dir)))
	{
		if (!is_temp_input(de->d_name))
		{
			continue;
		}

		row = malloc(sizeof(struct hwmon_sensor));
		if (!row)
		{
			perror("malloc");
			continue;
		}
		memset(row, 0, sizeof(struct hwmon_sensor));
		row->device = dev;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-truncation"
		snprintf(row->path_in, sizeof(row->path_in), "%s", de->d_name);
#pragma GCC diagnostic pop

		if (load_sensor_max(dev, row) != 0)
		{
			free(row);
			continue;
		}

		if (load_sensor_label(dev, row) != 0)
		{
			free(row);
			continue;
		}

		*next = row;
		next = &row->next;
	}

	return 0;
}

static int
load_device(struct hwmon_device *dev)
{
	DIR *dir;

	if (load_device_name(dev) != 0)
	{
		return 1;
	}

	dir = opendir(dev->path);
	if (!dir)
	{
		perror(dev->path);
		return 1;
	}

	if (load_device_sensors(dir, dev) != 0)
	{
		closedir(dir);
		return 1;
	}

	closedir(dir);
	return 0;
}

static int
hwmon_load_all(DIR *basedir, struct hwmon_device **dev)
{
	struct dirent *de;
	struct hwmon_device *row;

	while ((de = readdir(basedir)))
	{
		if (!strcmp(de->d_name, "."))
		{
			continue; /* ignore */
		}
		if (!strcmp(de->d_name, ".."))
		{
			continue; /* ignore */
		}

		row = malloc(sizeof(struct hwmon_device));
		if (!row)
		{
			perror("malloc");
			return 1;
		}
		memset(row, 0, sizeof(struct hwmon_device));

		snprintf(row->path, sizeof(row->path), "%s/%s", HWMON_BASE_PATH,
				de->d_name);

		if (load_device(row) != 0)
		{
			free(row);
			continue;
		}

		*dev = row;
		dev = &row->next;
	}

	return 0;
}

struct hwmon_device *
hwmon_load_devices()
{
	DIR *basedir;
	struct hwmon_device *dev;

	dev = NULL;

	basedir = opendir(HWMON_BASE_PATH);
	if (!basedir)
	{
		perror(HWMON_BASE_PATH);
		return NULL;
	}

	if (hwmon_load_all(basedir, &dev) != 0)
	{
		closedir(basedir);
		return NULL;
	}

	if (closedir(basedir) != 0)
	{
		perror(HWMON_BASE_PATH); /* we don't really care */
	}

	return dev;
}

static void
hwmon_free_sensors(struct hwmon_sensor *s)
{
	struct hwmon_sensor *tmp;

	while (s)
	{
		tmp = s->next;
		free(s);
		s = tmp;
	}
}

void
hwmon_free_devices(struct hwmon_device *dev)
{
	struct hwmon_device *tmp;

	while (dev)
	{
		tmp = dev->next;
		hwmon_free_sensors(dev->sensors);
		free(dev);
		dev = tmp;
	}
}

struct hwmon_sensor *
hwmon_dev_by_path(struct hwmon_device *dev, const char *path)
{
	char dir_buf[PATH_MAX], fn_buf[PATH_MAX];
	char *dir, *fn;
	struct hwmon_sensor *s;

	snprintf(dir_buf, sizeof(dir_buf), "%s", path);
	snprintf(fn_buf, sizeof(fn_buf), "%s", path);

	dir = dirname(dir_buf);
	fn = basename(fn_buf);

	for (; dev; dev = dev->next)
	{
		if (strcmp(dev->path, dir))
		{
			continue;
		}

		for (s = dev->sensors; s; s = s->next)
		{
			if (!strcmp(s->path_in, fn))
			{
				return s;
			}
		}
	}

	return NULL;
}

static struct hwmon_sensor *
better_sensor(struct hwmon_sensor *this, struct hwmon_sensor *that)
{
	if (!this)
	{
		return that;
	}

	/* coretemp would seem to be a sane default */
	if (!strcmp(this->device->name, "coretemp"))
	{
		if (strcmp(that->device->name, "coretemp"))
		{
			return this;
		}
	}
	else if (!strcmp(that->device->name, "coretemp"))
	{
		return that;
	}

	if ((this->temp_max == 0) && (that->temp_max != 0))
	{
		return that;
	}

	return this;
}

struct hwmon_sensor *
hwmon_best_sensor(struct hwmon_device *dev)
{
	struct hwmon_sensor *best, *s;

	best = NULL;

	for (; dev; dev = dev->next)
	{
		for (s = dev->sensors; s; s = s->next)
		{
			best = better_sensor(best, s);
		}
	}

	return best;
}
