/*-
 * redshift-ng - Automatically adjust display colour temperature according the Sun
 * 
 * Copyright (c) 2009-2018        Jon Lund Steffensen <jonlst@gmail.com>
 * Copyright (c) 2014-2016, 2025  Mattias Andrée <m@maandree.se>
 * 
 * redshift-ng 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.
 * 
 * redshift-ng 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 redshift-ng.  If not, see <http://www.gnu.org/licenses/>.
 */
#include "common.h"

#include <libcoopgamma.h>

#if defined(__clang__)
# pragma clang diagnostic ignored "-Wkeyword-macro"
#endif


struct coopgamma_output_id {
	char *edid;
	size_t index;
};


struct coopgamma_crtc_state {
	libcoopgamma_filter_t filter;
	libcoopgamma_ramps_t plain_ramps;
	size_t rampsize;
};


struct gamma_state {
	libcoopgamma_context_t ctx;
	struct coopgamma_crtc_state *crtcs;
	size_t n_crtcs;
	char **methods;
	char *method;
	char *site;
	int64_t priority;
	int list_outputs;
	struct coopgamma_output_id *outputs;
	size_t n_outputs;
	size_t a_outputs;
};


struct signal_blockage {int dummy;};


static int
unblocked_signal(int signo, struct signal_blockage *prev)
{
	/* TODO */
	(void) signo;
	(void) prev;
	return 0;
}


static int
restore_signal_blockage(int signo, const struct signal_blockage *blockage)
{
	/* TODO */
	(void) signo;
	(void) blockage;
	return 0;
}


static int
update(struct gamma_state *state)
{
	size_t i;
	for (i = 0; i < state->n_crtcs; i++)
		libcoopgamma_set_gamma_sync(&state->crtcs[i].filter, &state->ctx);
	return 0;
}


static void
print_error(struct gamma_state *state)
{
	unsigned long long int ec = (unsigned long long int)state->ctx.error.number;
	if (state->ctx.error.custom) {
		if (ec && state->ctx.error.description) {
			if (state->ctx.error.server_side)
				weprintf(_("Server-side error number %llu: %s."), ec, state->ctx.error.description);
			else
				weprintf(_("Client-side error number %llu: %s."), ec, state->ctx.error.description);
		} else if (ec) {
			if (state->ctx.error.server_side)
				weprintf(_("Server-side error number %llu."), ec);
			else
				weprintf(_("Client-side error number %llu."), ec);
		} else if (state->ctx.error.description) {
			if (state->ctx.error.server_side)
				weprintf(_("Server-side error: %s."), state->ctx.error.description);
			else
				weprintf(_("Client-side error: %s."), state->ctx.error.description);
		}
	} else if (state->ctx.error.description) {
		if (state->ctx.error.server_side)
			weprintf(_("Server-side error: %s."), state->ctx.error.description);
		else
			weprintf(_("Client-side error: %s."), state->ctx.error.description);
	} else {
		if (state->ctx.error.server_side)
			weprintf(_("Server-side error: %s."), strerror(state->ctx.error.number));
		else
			weprintf(_("Client-side error: %s."), strerror(state->ctx.error.number));
	}
}


static int
coopgamma_create(struct gamma_state **state_out)
{
	struct gamma_state *state;
	struct signal_blockage signal_blockage;

	state = *state_out = ecalloc(1, sizeof(**state_out));

	if (libcoopgamma_context_initialise(&state->ctx)) {
		weprintf("libcoopgamma_context_initialise:");
		return -1;
	}

	/* This is done this early to check if coopgamma is available */
	if (unblocked_signal(SIGCHLD, &signal_blockage) < 0)
		return -1;
	state->methods = libcoopgamma_get_methods();
	if (state->methods == NULL) {
		weprintf("libcoopgamma_get_methods:");
		if (restore_signal_blockage(SIGCHLD, &signal_blockage) < 0)
			exit(1);
		return -1;
	}
	if (restore_signal_blockage(SIGCHLD, &signal_blockage) < 0)
		return -1;

	state->priority = 0x0800000000000000LL;

	return 0;
}


static int
coopgamma_start(struct gamma_state *state)
{
	struct signal_blockage signal_blockage;
	libcoopgamma_lifespan_t lifespan;
	char** outputs;
	size_t i, j, n_outputs;
	int r;
	double d;

	switch (mode) {
	case PROGRAM_MODE_RESET:
		lifespan = LIBCOOPGAMMA_REMOVE;
		break;
	case PROGRAM_MODE_ONE_SHOT:
		lifespan = LIBCOOPGAMMA_UNTIL_REMOVAL;
		break;
	case PROGRAM_MODE_CONTINUAL:
		lifespan = LIBCOOPGAMMA_UNTIL_DEATH;
		break;
	default:
	case PROGRAM_MODE_PRINT:
		abort();
		break;
	}

	free(state->methods);
	state->methods = NULL;

	/* Connect to server */
	if (unblocked_signal(SIGCHLD, &signal_blockage) < 0)
		return -1;
	if (libcoopgamma_connect(state->method, state->site, &state->ctx) < 0) {
		if (errno)
			weprintf("libcoopgamma_connect:");
		else
			weprintf(_("libcoopgamma_connect: could not start coopgamma server."));
		if (restore_signal_blockage(SIGCHLD, &signal_blockage) < 0)
			exit(1);
		return -1;
	}
	if (restore_signal_blockage(SIGCHLD, &signal_blockage) < 0)
		return -1;
	free(state->method);
	state->method = NULL;
	free(state->site);
	state->site = NULL;

	/* Get available outputs */
	outputs = libcoopgamma_get_crtcs_sync(&state->ctx);
	for (n_outputs = 0; outputs[n_outputs]; n_outputs++);

	/* List available output if edid=list was used */
	if (state->list_outputs) {
		if (!outputs) {
			print_error(state);
			return -1;
		}
		printf(_("Available outputs:\n"));
		for (i = 0; outputs[i]; i++)
			printf("  %s\n", outputs[i]);
		if (ferror(stdout))
			eprintf("printf:");
		exit(0);
	}

	/* Translate crtc=N to edid=EDID */
	for (i = 0; i < state->n_outputs; i++) {
		if (state->outputs[i].edid)
			continue;
		if (state->outputs[i].index >= n_outputs) {
			weprintf(_("Monitor number %zu does not exist, available monitors are [0, %zu]"),
			         state->outputs[i].index, n_outputs - 1);
			return -1;
		}
		state->outputs[i].edid = estrdup(outputs[state->outputs[i].index]);
	}

	/* Use all outputs if none were specified */
	if (state->n_outputs == 0) {
		state->n_outputs = state->a_outputs = n_outputs;
		state->outputs = emalloc(n_outputs * sizeof(*state->outputs));
		for (i = 0; i < n_outputs; i++)
			state->outputs[i].edid = estrdup(outputs[i]);
	}

	free(outputs);

	/* Initialise information for each output */
	state->crtcs = ecalloc(state->n_outputs, sizeof(*state->crtcs));
	for (i = 0; i < state->n_outputs; i++) {
		libcoopgamma_crtc_info_t info;
		struct coopgamma_crtc_state *crtc = state->crtcs + state->n_crtcs;

		crtc->filter.priority = state->priority;
		crtc->filter.crtc = state->outputs[i].edid;
		crtc->filter.lifespan = lifespan;
#if defined(__GNUC__) && !defined(__clang__)
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wdiscarded-qualifiers"
#endif
		crtc->filter.class = PACKAGE "::redshift::standard";
#if defined(__GNUC__) && !defined(__clang__)
# pragma GCC diagnostic pop
#endif

		if (libcoopgamma_get_gamma_info_sync(crtc->filter.crtc, &info, &state->ctx) < 0) {
			int saved_errno = errno;
			weprintf(_("Failed to retrieve information for output `%s':\n"), outputs[i]);
			errno = saved_errno;
			print_error(state);
			return -1;
		}
		if (!info.cooperative) {
			weprintf(_("coopgamma is not available.\n"));
			return -1;
		}
		if (info.supported == LIBCOOPGAMMA_NO) {
			weprintf(_("Output `%s' does not support gamma adjustments, skipping."), outputs[i]);
			continue;
		}

		/* Get total size of the ramps */
		switch (info.depth) {
#define X(SUFFIX, TYPE, MAX, DEPTH)\
		case DEPTH:\
			crtc->rampsize = sizeof(TYPE);\
			break
		LIST_RAMPS_STOP_VALUE_TYPES(X, ;);
#undef X
		default:
			if (info.depth > 0) {
				weprintf(_("output `%s' uses an unsupported depth "
				           "for its gamma ramps: %i bits, skipping\n"),
				         outputs[i], info.depth);
			} else {
				weprintf(_("output `%s' uses an unrecognised depth, "
				           "for its gamma ramps, with the code %i, "
				           "skipping\n"), outputs[i], info.depth);
			}
			continue;
		}
		crtc->rampsize *= info.red_size + info.green_size + info.blue_size;

		crtc->filter.depth = info.depth;
		crtc->filter.ramps.u8.red_size = info.red_size;
		crtc->filter.ramps.u8.green_size = info.green_size;
		crtc->filter.ramps.u8.blue_size = info.blue_size;
		crtc->plain_ramps.u8.red_size = info.red_size;
		crtc->plain_ramps.u8.green_size = info.green_size;
		crtc->plain_ramps.u8.blue_size = info.blue_size;

		/* Initialise plain ramp and working ramp */
#define float f
#define double d
		switch (info.depth) {
#define X(SUFFIX, TYPE, MAX, DEPTH)\
		case DEPTH:\
			r = libcoopgamma_ramps_initialise(&crtc->filter.ramps.SUFFIX);\
			if (r < 0) {\
				perror("libcoopgamma_ramps_initialise");\
				return -1;\
			}\
			r = libcoopgamma_ramps_initialise(&crtc->plain_ramps.SUFFIX);\
			if (r < 0) {\
				perror("libcoopgamma_ramps_initialise");\
				return -1;\
			}\
			for (j = 0; j < crtc->plain_ramps.SUFFIX.red_size; j++) {\
				d = j;\
				d /= crtc->plain_ramps.SUFFIX.red_size;\
				crtc->plain_ramps.SUFFIX.red[j] = d * (MAX);\
			}\
			for (j = 0; j < crtc->plain_ramps.SUFFIX.green_size; j++) {\
				d = j;\
				d /= crtc->plain_ramps.SUFFIX.green_size;\
				crtc->plain_ramps.SUFFIX.green[j] = d * (MAX);\
			}\
			for (j = 0; j < crtc->plain_ramps.SUFFIX.blue_size; j++) {\
				d = j;\
				d /= crtc->plain_ramps.SUFFIX.blue_size;\
				crtc->plain_ramps.SUFFIX.blue[j] = d * (MAX);\
			}\
			break
		LIST_RAMPS_STOP_VALUE_TYPES(X, ;);
#undef X
		default:
			abort();
		}
#undef float
#undef double

		state->outputs[i].edid = NULL;
		state->n_crtcs++;
	}

	free(state->outputs);
	state->outputs = NULL;
	state->n_outputs = 0;

	return 0;
}


static void
coopgamma_free(struct gamma_state *state)
{
	free(state->methods);
	state->methods = NULL;
	free(state->method);
	state->method = NULL;
	free(state->site);
	state->site = NULL;

	while (state->n_crtcs--) {
		state->crtcs[state->n_crtcs].filter.class = NULL;
		libcoopgamma_filter_destroy(&state->crtcs[state->n_crtcs].filter);
		libcoopgamma_ramps_destroy(&state->crtcs[state->n_crtcs].plain_ramps);
	}
	state->n_crtcs = 0;
	free(state->crtcs);
	state->crtcs = NULL;

	libcoopgamma_context_destroy(&state->ctx, 1);
	while (state->n_outputs--)
		free(state->outputs[state->n_outputs].edid);
	state->n_outputs = 0;
	free(state->outputs);
	state->outputs = NULL;
}


static void
coopgamma_print_help(FILE *f)
{
	fputs(_("Adjust gamma ramps with coopgamma.\n"), f);
	fputs("\n", f);

	/* TRANSLATORS: coopgamma help output left column must not be translated */
	fputs(_("  edid=EDID      \tEDID of monitor to apply adjustments to, enter "
		"`list' to list available monitors\n"
		"  crtc=N         \tIndex of CRTC to apply adjustments to\n"
		"  priority=N     \tThe application order of the adjustments, "
		"default value is 576460752303423488.\n"
		"  method=METHOD  \tUnderlaying adjustment method, enter "
		"`list' to list available methods\n"
		"  display=DISPLAY\tThe display to apply adjustments to\n"),
	      f);
	fputs("\n", f);
}


static int
coopgamma_set_option(struct gamma_state *state, const char *key, const char *value)
{
	size_t i;
	char *end;
	long long int priority;

	if (!strcasecmp(key, "priority")) {
		errno = 0;
		priority = strtoll(value, &end, 10);
		if (errno || *end || priority < INT64_MIN || priority > INT64_MAX) {
			weprintf(_("Value of method parameter `crtc' must be a integer in [%lli, %lli]."),
			         (long long int)INT64_MIN, (long long int)INT64_MAX);
			return -1;
		}
		state->priority = priority;
	} else if (!strcasecmp(key, "method")) {
		if (state->method != NULL) {
			weprintf(_("Method parameter `method' can only be used once."));
			return -1;
		}
		if (!strcasecmp(value, "list")) {
			/* TRANSLATORS: coopgamma help output the word "coopgamma" must not be translated */
			printf(_("Available adjustment methods for coopgamma:\n"));
			for (i = 0; state->methods[i]; i++)
				printf("  %s\n", state->methods[i]);
			if (ferror(stdout))
				eprintf("printf:");
			exit(0);
		}
		state->method = estrdup(value);
	} else if (!strcasecmp(key, "display")) {
		if (state->site != NULL) {
			weprintf(_("Method parameter `display' can only be used once."));
			return -1;
		}
		state->site = estrdup(value);
	} else if (!strcasecmp(key, "edid") || !strcasecmp(key, "crtc")) {
		if (state->n_outputs == state->a_outputs) {
			state->a_outputs += 8;
			state->outputs = erealloc(state->outputs, state->a_outputs * sizeof(*state->outputs));
		}
		if (!strcasecmp(key, "edid")) {
			state->outputs[state->n_outputs].edid = estrdup(value);
			if (!strcasecmp(state->outputs[state->n_outputs].edid, "list"))
				state->list_outputs = 1;
		} else {
			state->outputs[state->n_outputs].edid = NULL;
			errno = 0;
			state->outputs[state->n_outputs].index = (size_t)strtoul(value, &end, 10);
			if (!*end && errno == ERANGE && state->outputs[state->n_outputs].index == SIZE_MAX) {
				state->outputs[state->n_outputs].index = SIZE_MAX;
			} else if (errno || *end) {
				weprintf(_("Value of method parameter `crtc' must be a non-negative integer."));
				return -1;
			}
		}
		state->n_outputs++;
	} else {
		weprintf(_("Unknown method parameter: `%s'."), key);
		return -1;
	}

	return 0;
}


static void
coopgamma_restore(struct gamma_state *state)
{
	size_t i;
	for (i = 0; i < state->n_crtcs; i++)
		state->crtcs[i].filter.lifespan = LIBCOOPGAMMA_REMOVE;
	update(state);
	for (i = 0; i < state->n_crtcs; i++)
		state->crtcs[i].filter.lifespan = LIBCOOPGAMMA_UNTIL_DEATH;
}


static int
coopgamma_apply(struct gamma_state *state, const struct colour_setting *setting, int perserve)
{
	libcoopgamma_filter_t *filter;
	libcoopgamma_filter_t *last_filter = NULL;
	size_t i;

	(void) perserve;

	for (i = 0; i < state->n_crtcs; i++, last_filter = filter) {
		filter = &state->crtcs[i].filter;

		/* Copy ramps for previous CRTC if its ramps is of same size and depth */
		if (last_filter &&
		    last_filter->ramps.u8.red_size   == filter->ramps.u8.red_size &&
		    last_filter->ramps.u8.green_size == filter->ramps.u8.green_size &&
		    last_filter->ramps.u8.blue_size  == filter->ramps.u8.blue_size) {
			memcpy(filter->ramps.u8.red, last_filter->ramps.u8.red, state->crtcs[i].rampsize);
			continue;
		}

		/* Otherwise, create calculate the ramps */
		memcpy(filter->ramps.u8.red, state->crtcs[i].plain_ramps.u8.red, state->crtcs[i].rampsize);
		switch (filter->depth) {
#define X(SUFFIX, TYPE, MAX, DEPTH)\
		case DEPTH:\
			fill_ramps_##SUFFIX((void *)(filter->ramps.u8.red),\
			                    (void *)(filter->ramps.u8.green),\
			                    (void *)(filter->ramps.u8.blue),\
			                    filter->ramps.u8.red_size,\
			                    filter->ramps.u8.green_size,\
			                    filter->ramps.u8.blue_size,\
			                    setting);\
			break
		LIST_RAMPS_STOP_VALUE_TYPES(X, ;);
#undef X
		default:
			abort();
		}
	}

	return update(state);
}


const struct gamma_method coopgamma_gamma_method = GAMMA_METHOD_INIT("coopgamma", 1, 0, coopgamma);