/*-
 * 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 <xf86drm.h>
#include <xf86drmMode.h>

#ifndef O_CLOEXEC
# define O_CLOEXEC  02000000
#endif


struct drm_crtc_state {
	int crtc_num;
	int crtc_id;
	int gamma_size;
	uint16_t *r_gamma;
	uint16_t *g_gamma;
	uint16_t *b_gamma;
};


struct gamma_state {
	int card_num;
	int crtc_num;
	int fd;
	drmModeRes *res;
	struct drm_crtc_state *crtcs;
};


static int
drm_create(struct gamma_state **state_out)
{
	struct gamma_state *state;
	state = *state_out = emalloc(sizeof(**state_out));
	state->card_num = 0;
	state->crtc_num = -1;
	state->fd = -1;
	state->res = NULL;
	state->crtcs = NULL;
	return 0;
}


static int
drm_start(struct gamma_state *state)
{
	/* Acquire access to a graphics card. */
	char pathname[sizeof(DRM_DIR_NAME"")-1U + sizeof(DRM_DEV_NAME"")-1U + 3U * sizeof(int) + 2U];
	int crtc_count;
	struct drm_crtc_state *crtcs;

	sprintf(pathname, DRM_DEV_NAME, DRM_DIR_NAME, state->card_num);

	state->fd = open(pathname, O_RDWR | O_CLOEXEC);
	if (state->fd < 0) {
		/* TODO check if access permissions, normally root or membership of the video group is required. */
		weprintf(_("Failed to open DRM device `%s': %s."), pathname, strerror(errno));
		return -1;
	}

	/* Acquire mode resources. */
	state->res = drmModeGetResources(state->fd);
	if (state->res == NULL) {
		weprintf(_("Failed to get DRM mode resources."));
		close(state->fd);
		state->fd = -1;
		return -1;
	}

	/* Create entries for selected CRTCs. */
	crtc_count = state->res->count_crtcs;
	if (state->crtc_num >= 0) {
		if (state->crtc_num >= crtc_count) {
			if (crtc_count > 1)
				weprintf(_("CRTC %i does not exist, valid CRTCs are [0, %i]."), state->crtc_num, crtc_count-1);
			else
				weprintf(_("CRTC %i does not exist, only CRTC 0 exists."), state->crtc_num);
			close(state->fd);
			state->fd = -1;
			drmModeFreeResources(state->res);
			state->res = NULL;
			return -1;
		}

		state->crtcs = malloc(2 * sizeof(struct drm_crtc_state));
		state->crtcs[1].crtc_num = -1;

		state->crtcs->crtc_num = state->crtc_num;
		state->crtcs->crtc_id = -1;
		state->crtcs->gamma_size = -1;
		state->crtcs->r_gamma = NULL;
		state->crtcs->g_gamma = NULL;
		state->crtcs->b_gamma = NULL;
	} else {
		int crtc_num;
		state->crtcs = malloc((crtc_count + 1) * sizeof(struct drm_crtc_state));
		state->crtcs[crtc_count].crtc_num = -1;
		for (crtc_num = 0; crtc_num < crtc_count; crtc_num++) {
			state->crtcs[crtc_num].crtc_num = crtc_num;
			state->crtcs[crtc_num].crtc_id = -1;
			state->crtcs[crtc_num].gamma_size = -1;
			state->crtcs[crtc_num].r_gamma = NULL;
			state->crtcs[crtc_num].g_gamma = NULL;
			state->crtcs[crtc_num].b_gamma = NULL;
		}
	}

	/* Load CRTC information and gamma ramps. */
	crtcs = state->crtcs;
	for (; crtcs->crtc_num >= 0; crtcs++) {
		drmModeCrtc *crtc_info;
		crtcs->crtc_id = state->res->crtcs[crtcs->crtc_num];
		crtc_info = drmModeGetCrtc(state->fd, crtcs->crtc_id);
		if (crtc_info == NULL) {
			weprintf(_("CRTC %i lost, skipping"), crtcs->crtc_num);
			continue;
		}
		crtcs->gamma_size = crtc_info->gamma_size;
		drmModeFreeCrtc(crtc_info);
		if (crtcs->gamma_size <= 1) {
			weprintf(_("Could not get gamma ramp size for CRTC %i on graphics card"
			           " %i, ignoring device."), crtcs->crtc_num, state->card_num);
			continue;
		}
		/* Valgrind complains about us reading uninitialize memory if we just use malloc. */
		crtcs->r_gamma = calloc(3 * crtcs->gamma_size, sizeof(uint16_t));
		crtcs->g_gamma = crtcs->r_gamma + crtcs->gamma_size;
		crtcs->b_gamma = crtcs->g_gamma + crtcs->gamma_size;
		if (crtcs->r_gamma) {
			int r = drmModeCrtcGetGamma(state->fd, crtcs->crtc_id, crtcs->gamma_size,
						    crtcs->r_gamma, crtcs->g_gamma, crtcs->b_gamma);
			if (r < 0) {
				weprintf(_("DRM could not read gamma ramps on CRTC %i on graphics card"
				           " %i, ignoring device."), crtcs->crtc_num, state->card_num);
				free(crtcs->r_gamma);
				crtcs->r_gamma = NULL;
			}
		} else {
			weprintf("malloc:");
			drmModeFreeResources(state->res);
			state->res = NULL;
			close(state->fd);
			state->fd = -1;
			while (crtcs-- != state->crtcs)
				free(crtcs->r_gamma);
			free(state->crtcs);
			state->crtcs = NULL;
			return -1;
		}
	}

	return 0;
}


static void
drm_restore(struct gamma_state *state)
{
	struct drm_crtc_state *crtcs = state->crtcs;
	while (crtcs->crtc_num >= 0) {
		if (crtcs->r_gamma) {
			drmModeCrtcSetGamma(state->fd, crtcs->crtc_id, crtcs->gamma_size,
					    crtcs->r_gamma, crtcs->g_gamma, crtcs->b_gamma);
		}
		crtcs++;
	}
}


static void
drm_free(struct gamma_state *state)
{
	if (state->crtcs) {
		struct drm_crtc_state *crtcs = state->crtcs;
		while (crtcs->crtc_num >= 0) {
			free(crtcs->r_gamma);
			crtcs->crtc_num = -1;
			crtcs++;
		}
		free(state->crtcs);
		state->crtcs = NULL;
	}
	if (state->res) {
		drmModeFreeResources(state->res);
		state->res = NULL;
	}
	if (state->fd >= 0) {
		close(state->fd);
		state->fd = -1;
	}

	free(state);
}


static void
drm_print_help(FILE *f)
{
	fputs(_("Adjust gamma ramps with Direct Rendering Manager.\n"), f);
	fputs("\n", f);

	/* TRANSLATORS: DRM help output
	   left column must not be translated */
	fputs(_("  card=N\tGraphics card to apply adjustments to\n"
		"  crtc=N\tCRTC to apply adjustments to\n"), f); /* TODO list is not supported */
	fputs("\n", f);
}


static int
drm_set_option(struct gamma_state *state, const char *key, const char *value)
{
	if (!strcasecmp(key, "card")) {
		state->card_num = atoi(value);
	} else if (!strcasecmp(key, "crtc")) {
		if (!strcasecmp(value, "all")) {
			state->crtc_num = -1;
		} else {
			state->crtc_num = atoi(value);
			if (state->crtc_num < 0) {
				weprintf(_("CRTC must be a non-negative integer"));
				return -1;
			}
		}
	} else {
		weprintf(_("Unknown method parameter: `%s'."), key);
		return -1;
	}

	return 0;
}


static int
drm_apply(struct gamma_state *state, const struct colour_setting *setting, int preserve)
{
	struct drm_crtc_state *crtcs = state->crtcs;
	uint32_t last_gamma_size = 0;
	uint16_t *r_gamma = NULL;
	uint16_t *g_gamma = NULL;
	uint16_t *b_gamma = NULL;
	uint16_t value;
	uint32_t i, ramp_size;

	(void) preserve; /* TODO */

	for (; crtcs->crtc_num >= 0; crtcs++) {
		if (crtcs->gamma_size <= 1)
			continue;
		if (crtcs->gamma_size != last_gamma_size) {
			if (crtcs->gamma_size > last_gamma_size) {
				r_gamma = erealloc(r_gamma, 3 * crtcs->gamma_size * sizeof(uint16_t));
				g_gamma = r_gamma + crtcs->gamma_size;
				b_gamma = g_gamma + crtcs->gamma_size;
			}
			last_gamma_size = crtcs->gamma_size;
		}

		/* Initialize gamma ramps to pure state */
		ramp_size = crtcs->gamma_size;
		for (i = 0; i < ramp_size; i++) {
			value = (uint16_t)((double)i / (ramp_size - 1) * UINT16_MAX);
			r_gamma[i] = value;
			g_gamma[i] = value;
			b_gamma[i] = value;
		}

		fill_ramps_u16(r_gamma, g_gamma, b_gamma, crtcs->gamma_size, crtcs->gamma_size, crtcs->gamma_size, setting);
		drmModeCrtcSetGamma(state->fd, crtcs->crtc_id, crtcs->gamma_size, r_gamma, g_gamma, b_gamma);
	}

	free(r_gamma);

	return 0;
}


const struct gamma_method drm_gamma_method = GAMMA_METHOD_INIT("drm", 0, 0, drm);