#include <sys/types.h>
#include <sys/utsname.h>
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#include <errno.h>
#include <fnmatch.h>
#include <limits.h>
#include <getopt.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#ifdef ENABLE_SYSTEMD
# include <systemd/sd-bus.h>
#endif

static char *path = "/sys/class";
static char *classes[] = { "backlight", "leds", NULL };

static char *run_dir = "/tmp/brightnessctl";

struct value;
struct device;

enum operation;

static void fail(char *, ...);
static void usage(void);
#define cat_with(...) _cat_with(__VA_ARGS__, NULL)
static char *_cat_with(char, ...);
static char *dir_child(char *, char*);
static char *device_path(struct device *);
static char *class_path(char *);
static unsigned int calc_value(struct device *, struct value *);
static int apply_operation(struct device *, enum operation, struct value *);
static bool parse_value(struct value *, char *);
static bool do_write_device(struct device *);
static bool read_device(struct device *, char *, char *);
static int read_class(struct device **, char *);
static int read_devices(struct device **);
static void print_device(struct device *);
static void list_devices(struct device **);
static struct device *find_device(struct device **, char *);
static bool save_device_data(struct device *);
static bool restore_device_data(struct device *);
static bool ensure_dir(char *);
static bool ensure_dev_dir(struct device *);
#define ensure_run_dir() ensure_dir(run_dir)

#ifdef ENABLE_SYSTEMD
static bool logind_set_brightness(struct device *);
#endif

struct device {
	char *class;
	char *id;
	unsigned int curr_brightness;
	unsigned int max_brightness;
};

enum value_type { ABSOLUTE, RELATIVE };
enum delta_type { DIRECT, DELTA };
enum sign { PLUS, MINUS };

struct value {
	unsigned long val;
	enum value_type v_type;
	enum delta_type d_type;
	enum sign sign;
};

enum operation { INFO, GET, MAX, SET };

struct params {
	char *class;
	char *device;
	struct value val;
	long min;
	enum operation operation;
	bool quiet;
	bool list;
	bool pretend;
	bool mach;
	bool save;
	bool restore;
	float exponent;
};

static struct params p;

static const struct option options[] = {
	{"class", required_argument, NULL, 'c'},
	{"device", required_argument, NULL, 'd'},
	{"help", no_argument, NULL, 'h'},
	{"list", no_argument, NULL, 'l'},
	{"machine-readable", no_argument, NULL, 'm'},
	{"min-value", optional_argument, NULL, 'n'},
	{"exponent", optional_argument, NULL, 'e'},
	{"quiet", no_argument, NULL, 'q'},
	{"pretend", no_argument, NULL, 'p'},
	{"restore", no_argument, NULL, 'r'},
	{"save", no_argument, NULL, 's'},
	{"version", no_argument, NULL, 'V'},
	{NULL,}
};

static bool (*write_device)(struct device *) = do_write_device;

int main(int argc, char **argv) {
	struct device *devs[255];
	struct device *dev;
	struct utsname name;
	char *dev_name, *file_path, *sys_run_dir;
	int n, c, phelp = 0;
	if (uname(&name))
		fail("Unable to determine current OS. Exiting!\n");
	if (strcmp(name.sysname, "Linux"))
		fail("This program only supports Linux.\n");
	p.exponent = 1;
	while (1) {
		if ((c = getopt_long(argc, argv, "lqpmn::e::srhVc:d:", options, NULL)) < 0)
			break;
		switch (c) {
		case 'l':
			p.list = true;
			break;
		case 'q':
			p.quiet = true;
			break;
		case 'p':
			p.pretend = true;
			break;
		case 's':
			p.save = true;
			break;
		case 'r':
			p.restore = true;
			break;
		case 'm':
			p.mach = true;
			break;
		case 'n':
			if (optarg)
				p.min = atol(optarg);
			else
				p.min = 1;
			break;
		case 'e':
			if (optarg)
				p.exponent = atof(optarg);
			else
				p.exponent = 4;
			break;
		case 'h':
			usage();
			exit(EXIT_SUCCESS);
		case 'c':
			p.class = strdup(optarg);
			break;
		case 'd':
			p.device = strdup(optarg);
			break;
		case 'V':
			printf("%s\n", VERSION);
			exit(0);
			break;
		default:
			phelp++;
		}
	}
	if (phelp) {
		usage();
		exit(EXIT_FAILURE);
	}
	argc -= optind;
	argv += optind;
	if (p.class) {
		if (!(n = read_class(devs, p.class)))
			fail("Failed to read any devices of class '%s'.\n", p.class);
	} else {
		if (!(n = read_devices(devs)))
			fail("Failed to read any devices.\n");
	}
	devs[n] = NULL;
	if (p.list) {
		list_devices(devs);
		return 0;
	}
	dev_name = p.device;
	if (!dev_name)
		dev_name = devs[0]->id;
	if (argc == 0)
		p.operation = INFO;
	else switch (argv[0][0]) {
	case 'm':
		p.operation = MAX; break;
	case 's':
		p.operation = SET; break;
	case 'g':
		p.operation = GET; break;
	default:
	case 'i':
		p.operation = INFO; break;
	}
	argc--;
	argv++;
	if (p.operation == SET && argc == 0)
		fail("You need to provide a value to set.\n");
	if (p.operation == SET && !parse_value(&p.val, argv[0]))
		fail("Invalid value given");
	if (!(dev = find_device(devs, dev_name)))
		fail("Device '%s' not found.\n", dev_name);
	if ((p.operation == SET || p.restore) && !p.pretend && geteuid()) {
		errno = 0;
		file_path = cat_with('/', path, dev->class, dev->id, "brightness");
		if (access(file_path, W_OK)) {
#ifdef ENABLE_SYSTEMD
			write_device = logind_set_brightness;
#else
			perror("Can't modify brightness");
			fail("\nYou should run this program with root privileges.\n"
				"Alternatively, get write permissions for device files.\n");
#endif
		}
		free(file_path);
	}
	if ((sys_run_dir = getenv("XDG_RUNTIME_DIR")))
		run_dir = dir_child(sys_run_dir, "brightnessctl");
	if (p.save)
		if (!save_device_data(dev))
			fprintf(stderr, "Could not save data for device '%s'.\n", dev->id);
	if (p.restore) {
		if (restore_device_data(dev))
			write_device(dev);
	}
	return apply_operation(dev, p.operation, &p.val);
}

int apply_operation(struct device *dev, enum operation operation, struct value *val) {
	switch (operation) {
	case INFO:
		print_device(dev);
		return 0;
	case GET:
		fprintf(stdout, "%u\n", dev->curr_brightness);
		return 0;
	case MAX:
		fprintf(stdout, "%u\n", dev->max_brightness);
		return 0;
	case SET:
		dev->curr_brightness = calc_value(dev, val);
		if (!p.pretend)
			if (!write_device(dev))
				goto fail;
		if (!p.quiet) {
			if (!p.mach)
				fprintf(stdout, "Updated device '%s':\n", dev->id);
			print_device(dev);
		}
		return 0;
	/* FALLTHRU */
	fail:
	default:
		return 1;
	}
}

bool parse_value(struct value *val, char *str) {
	long n;
	char c;
	char *buf;
	errno = 0;
	val->v_type = ABSOLUTE;
	val->d_type = DIRECT;
	val->sign = PLUS;
	if (!str || !*str)
		return false;
	if (*str == '+' || *str == '-') {
		val->sign = *str == '+' ? PLUS : MINUS;
		val->d_type = DELTA;
		str++;
	}
	n = strtol(str, &buf, 10);
	if (errno || buf == str)
		return false;
	val->val = labs(n) % LONG_MAX;
	while ((c = *(buf++))) switch(c) {
	case '+':
		val->sign = PLUS;
		val->d_type = DELTA;
		break;
	case '-':
		val->sign = MINUS;
		val->d_type = DELTA;
		break;
	case '%':
		val->v_type = RELATIVE;
		break;
	}
	return true;
}

struct device *find_device(struct device **devs, char *name) {
	struct device *dev;
	while ((dev = *(devs++)))
		if (!fnmatch(name, dev->id, 0))
			return dev;
	return NULL;
}

void list_devices(struct device **devs) {
	struct device *dev;
	if (!p.mach)
		fprintf(stdout, "Available devices:\n");
	while ((dev = *(devs++)))
		print_device(dev);
}

float val_to_percent(float val, struct device *d, bool rnd) {
	if (val < 0)
		return 0;
	float ret = powf(val / d->max_brightness, 1.0f / p.exponent) * 100;
	return rnd ? roundf(ret) : ret;
}

unsigned long percent_to_val(float percent, struct device *d) {
	return roundf(powf(percent / 100, p.exponent) * d->max_brightness);
}

void print_device(struct device *dev) {
	char *format = p.mach ? "%s,%s,%d,%d%%,%d\n" :
		"Device '%s' of class '%s':\n\tCurrent brightness: %d (%d%%)\n\tMax brightness: %d\n\n";
	fprintf(stdout, format,
		dev->id, dev->class,
		dev->curr_brightness,
		(int) val_to_percent(dev->curr_brightness, dev, true),
		dev->max_brightness);
}

unsigned int calc_value(struct device *d, struct value *val) {
	long new = d->curr_brightness;
	if (val->d_type == DIRECT) {
		new = val->v_type == ABSOLUTE ? val->val : percent_to_val(val->val, d);
		goto apply;
	}
	long mod = val->val;
	if (val->sign == MINUS)
		mod *= -1;
	if (val->v_type == RELATIVE) {
		mod = percent_to_val(val_to_percent(d->curr_brightness, d, false) + mod, d) - d->curr_brightness;
		if (val->val != 0 && mod == 0)
			mod = val->sign == PLUS ? 1 : -1;
	}
	new += mod;
apply:
	if (new < p.min)
		new = p.min;
	if (new < 0)
		new = 0;
	if (new > d->max_brightness)
		new = d->max_brightness;
	return new;
}

#ifdef ENABLE_SYSTEMD

bool logind_set_brightness(struct device *d) {
	sd_bus *bus = NULL;
	int r = sd_bus_default_system(&bus);
	if (r < 0) {
		fprintf(stderr, "Can't connect to system bus: %s\n", strerror(-r));
		return false;
	}

	r = sd_bus_call_method(bus,
			       "org.freedesktop.login1",
			       "/org/freedesktop/login1/session/auto",
			       "org.freedesktop.login1.Session",
			       "SetBrightness",
			       NULL,
			       NULL,
			       "ssu",
			       d->class,
			       d->id,
			       d->curr_brightness);
	if (r < 0)
		fprintf(stderr, "Failed to set brightness: %s\n", strerror(-r));

	sd_bus_unref(bus);

	return r >= 0;
}

#endif

bool do_write_device(struct device *d) {
	FILE *f;
	char c[16];
	size_t s = sprintf(c, "%u", d->curr_brightness);
	errno = 0;
	if (s <= 0) {
		errno = EINVAL;
		goto fail;
	}
	if ((f = fopen(dir_child(device_path(d), "brightness"), "w"))) {
		if (fwrite(c, 1, s, f) < s)
			goto close;
	} else
		goto fail;
	errno = 0;
close:
	fclose(f);
fail:
	if (errno)
		perror("Error writing device");
	return !errno;
}

bool read_device(struct device *d, char *class, char *id) {
	DIR *dirp;
	FILE *f;
	char *dev_path = NULL;
	char *ent_path;
	int error = 0;
	struct dirent *ent;
	bool cur;
	d->class = strdup(class);
	d->id = strdup(id);
	dev_path = device_path(d);
	if (!(dirp = opendir(dev_path)))
		goto dfail;
	while ((ent = readdir(dirp))) {
		if (!strcmp(ent->d_name, ".") && !strcmp(ent->d_name, ".."))
			continue;
		if ((cur = !strcmp(ent->d_name, "brightness")) ||
				!strcmp(ent->d_name, "max_brightness")) {
			if (!(f = fopen(ent_path = dir_child(dev_path, ent->d_name), "r")))
				goto fail;
			clearerr(f);
			if (fscanf(f, "%u", cur ? &d->curr_brightness : &d->max_brightness) == EOF) {
				fprintf(stderr, "End-of-file reading %s of device '%s'.",
						cur ? "brightness" : "max brightness", d->id);
				error++;
			} else if (ferror(f)) {
				fprintf(stderr, "Error reading %s of device '%s': %s.",
						cur ? "brightness" : "max brightness", d->id, strerror(errno));
				error++;
			}
			fclose(f);
			free(ent_path);
			ent_path = NULL;
		}
	}
	errno = 0;
fail:
	closedir(dirp);
dfail:
	free(dev_path);
	free(ent_path);
	if (errno) {
		perror("Error reading device");
		error++;
	}
	return !error;
}

int read_class(struct device **devs, char *class) {
	DIR *dirp;
	struct dirent *ent;
	struct device *dev;
	char *c_path;
	int cnt = 0;
	dirp = opendir(c_path = class_path(class));
	if (!dirp)
		return 0;
	while ((ent = readdir(dirp))) {
		if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, ".."))
			continue;
		dev = malloc(sizeof(struct device));
		if (!read_device(dev, class, ent->d_name)) {
			free(dev);
			continue;
		}
		devs[cnt++] = dev;
	}
	closedir(dirp);
	free(c_path);
	return cnt;
}

int read_devices(struct device **devs) {
	size_t n = 0;
	char *class;
	int cnt = 0;
	while ((class = classes[n++]))
		cnt += read_class(devs + cnt, class);
	return cnt;
}

bool save_device_data(struct device *dev) {
	char c[16];
	size_t s = sprintf(c, "%u", dev->curr_brightness);
	char *d_path = cat_with('/', run_dir, dev->class, dev->id);
	FILE *fp;
	mode_t old = 0;
	int error = 0;
	errno = 0;
	if (s <= 0) {
		fprintf(stderr, "Error converting device data.");
		error++;
		goto fail;
	}
	if (!ensure_dev_dir(dev))
		goto fail;
	old = umask(0);
	fp = fopen(d_path, "w");
	umask(old);
	if (!fp)
		goto fail;
	if (fwrite(c, 1, s, fp) < s) {
		fprintf(stderr, "Error writing to '%s'.\n", d_path);
		error++;
	}
	fclose(fp);
fail:
	free(d_path);
	if (errno) {
		perror("Error saving device data");
		error++;
	}
	return !error;
}

bool restore_device_data(struct device *dev) {
	char buf[16];
	char *filename = cat_with('/', run_dir, dev->class, dev->id);
	char *end;
	FILE *fp;
	memset(buf, 0, 16);
	errno = 0;
	if (!(fp = fopen(filename, "r")))
		goto fail;
	if (!fread(buf, 1, 15, fp))
		goto rfail;
	dev->curr_brightness = strtol(buf, &end, 10);
	if (end == buf)
		errno = EINVAL;
rfail:
	fclose(fp);
fail:
	free(filename);
	if (errno) {
		perror("Error restoring device data");
		return false;
	}
	return true;
}


bool ensure_dir(char *dir) {
	struct stat sb;
	if (stat(dir, &sb)) {
		if (errno != ENOENT)
			return false;
		errno = 0;
		if (mkdir(dir, 0777)) {
			return false;
		}
		if (stat(dir, &sb))
			return false;
	}
	if (!S_ISDIR(sb.st_mode)) {
		errno = ENOTDIR;
		return false;
	}
	return true;
}

bool ensure_dev_dir(struct device *dev) {
	char *cpath;
	bool ret;
	if (!ensure_run_dir())
		return false;
	cpath = dir_child(run_dir, dev->class);
	ret = ensure_dir(cpath);
	free(cpath);
	return ret;
}

char *_cat_with(char c, ...) {
	size_t size = 32;
	size_t length = 0;
	char *buf = calloc(1, size + 1);
	char *curr;
	char split[2] = {c, '\0'};
	va_list va;
	va_start(va, c);
	curr = va_arg(va, char *);
	while (curr) {
		length += strlen(curr);
		while (length + 2 > size)
			buf = realloc(buf, size *= 2);
		strcat(buf, curr);
		if ((curr = va_arg(va, char*))) {
			length++;
			strcat(buf, split);
		}
	}
	return buf;
}

char *dir_child(char *parent, char *child) {
	return cat_with('/', parent, child);
}

char *device_path(struct device *dev) {
	return cat_with('/', path, dev->class, dev->id);
}

char *class_path(char *class) {
	return dir_child(path, class);
}

void fail(char *err_msg, ...) {
	va_list va;
	va_start(va, err_msg);
	vfprintf(stderr, err_msg, va);
	va_end(va);
	exit(EXIT_FAILURE);
}

void usage() {
	fprintf(stderr, "brightnessctl %s - read and control device brightness.\n\n", VERSION);
	fprintf(stderr,
"Usage: brightnessctl [options] [operation] [value]\n\
\n\
Options:\n\
  -l, --list\t\t\tlist devices with available brightness controls.\n\
  -q, --quiet\t\t\tsuppress output.\n\
  -p, --pretend\t\t\tdo not perform write operations.\n\
  -m, --machine-readable\tproduce machine-readable output.\n\
  -n, --min-value\t\tset minimum brightness, defaults to 1.\n\
  -e, --exponent[=K]\t\tchanges percentage curve to exponential.\n\
  -s, --save\t\t\tsave previous state in a temporary file.\n\
  -r, --restore\t\t\trestore previous saved state.\n\
  -h, --help\t\t\tprint this help.\n\
  -d, --device=DEVICE\t\tspecify device name (can be a wildcard).\n\
  -c, --class=CLASS\t\tspecify device class.\n\
  -V, --version\t\t\tprint version and exit.\n\
\n\
Operations:\n\
  i, info\t\t\tget device info.\n\
  g, get\t\t\tget current brightness of the device.\n\
  m, max\t\t\tget maximum brightness of the device.\n\
  s, set VALUE\t\t\tset brightness of the device.\n\
\n\
Valid values:\n\
  specific value\t\tExample: 500\n\
  percentage value\t\tExample: 50%%\n\
  specific delta\t\tExample: 50- or +10\n\
  percentage delta\t\tExample: 50%%- or +10%%\n\
\n");
}

