/*-
 * 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"


/**
 * The number of milliseconds to sleep normally between colour updates
 */
#define SLEEP_DURATION  5000U

/**
 * The number of milliseconds to sleep between each step during
 * fade between large colour settings
 */
#define SLEEP_DURATION_SHORT  25U

/**
 * The fade time, for when making large changes in colour
 * settings, divided by `SLEEP_DURATION_SHORT`
 */
#define FADE_LENGTH  160U


char *argv0;


/**
 * The user's current geographical location
 */
static struct location location;

/**
 * Whether the location provider is available
 */
static int location_available;

/**
 * State of location provider, `NULL` if none
 */
static LOCATION_STATE *provider_state;

/**
 * Locaiton provider functions
 */
static const struct location_provider *provider;

/**
 * State of the gamma ramp adjustment method, `NULL` if none (print mode)
 */
static GAMMA_STATE *method_state;

/**
 * Gamma ramp adjustment functions
 */
static const struct gamma_method *method;


/**
 * Suspend the process for a short time
 *
 * The process may be resumed earily, specifically
 * if it receives a signal
 * 
 * @param  msecs  The number of milliseconds to sleep
 */
static void
millisleep(unsigned int msecs)
{
#ifdef WINDOWS
	Sleep(msecs); /* TODO [Windows] not interruptible */
#else
	struct timespec ts;
	ts.tv_sec = (time_t)(msecs / 1000U);
	ts.tv_nsec = (long)(msecs % 1000U) * 1000000L;
	nanosleep(&ts, NULL);
#endif
}


/**
 * Get the number of seconds since midnight
 * 
 * @return  The number of seconds since midnight
 */
static time_t
get_time_since_midnight(void)
{
	time_t t = time(NULL);
	struct tm tm;
	localtime_r(&t, &tm);
	t  = (time_t)tm.tm_sec;
	t += (time_t)tm.tm_min * 60;
	t += (time_t)tm.tm_hour * 3600;
	return t;
}


/**
 * Print the current period of the day
 * 
 * @param  period     The current period of the day
 * @param  day_level  The current dayness level
 */
static void
print_period(enum period period, double day_level)
{
	static const char *period_names[] = {
		/* TRANSLATORS: Name printed when period of day is unknown */
		[PERIOD_NONE]       = N_("None"),
		[PERIOD_DAYTIME]    = N_("Daytime"),
		[PERIOD_NIGHT]      = N_("Night"),
		[PERIOD_TRANSITION] = N_("Transition")
	};

	if (period == PERIOD_TRANSITION)
		printf(_("Period: %s (%.2f%% day)\n"), gettext(period_names[period]), day_level * 100);
	else
		printf(_("Period: %s\n"), gettext(period_names[period]));
}


/**
 * Get the current period of day and the colour settings
 * applicable to the current time of the day
 * 
 * @param  colour_out     Output parameter for the colour settings
 * @param  period_out     Output parameter for the period of the day
 * @param  day_level_out  Output parameter for the dayness level
 */
static void
get_colour_settings(struct colour_setting *colour_out, enum period *period_out, double *day_level_out)
{
	time_t time_offset;
	double t, elevation;

	/* Get dayness level */
	if (scheme.type == CLOCK_SCHEME) {
		time_offset = get_time_since_midnight();
		while (time_offset >= scheme.time.periods->next->start)
			scheme.time.periods = scheme.time.periods->next;
		time_offset -= scheme.time.periods->start;
		if (time_offset < 0)
			time_offset += ONE_DAY;
		t = (double)time_offset;
		*day_level_out = fma(t, scheme.time.periods->diff_over_duration, scheme.time.periods->day_level);

	} else if (scheme.type == SOLAR_SCHEME) {
		if (libred_solar_elevation(location.latitude, location.longitude, &elevation))
			eprintf("libred_solar_elevation:");
		if (verbose) {
			/* TRANSLATORS: Append degree symbol if possible. */
			printf(_("Solar elevation: %f\n"), elevation);
		}
		*day_level_out = (elevation - scheme.elevation.low) / scheme.elevation.range;
		/* TODO ensure scheme.elevation.range==0 is supported */

	} else {
		/* Static scheme, no dayness-level or peroid; day_settings == nigh_settings, use either */
		*day_level_out = FNAN;
		*period_out = PERIOD_NONE;
		*colour_out = day_settings;
		return;
	}

	/* Clamp dayness level and get colour */
	if (*day_level_out <= 0.0) {
		*day_level_out = 0.0;
		*period_out = PERIOD_NIGHT;
		*colour_out = night_settings;

	} else if (*day_level_out >= 1.0) {
		*day_level_out = 1.0;
		*period_out = PERIOD_DAYTIME;
		*colour_out = day_settings;

	} else {
		*period_out = PERIOD_TRANSITION;
		interpolate_colour_settings(&night_settings, &day_settings, *day_level_out, colour_out);
	}
}


/**
 * Easing function used for fade effect
 * 
 * See https://github.com/mietek/ease-tween
 * 
 * @param   t  Raw fade progress
 * @return     Fade progress to apply
 */
GCC_ONLY(__attribute__((__const__)))
static double
ease_fade(double t)
{
	if (t <= 0) return 0;
	if (t >= 1) return 1;
	return 1.0042954579734844 * exp(-6.4041738958415664 * exp(-7.2908241330981340 * t));
}


#ifndef WINDOWS /* we don't have poll on Windows, but neither do with have any dynamic location providers  */
/**
 * Get the current location
 * 
 * The function will return once any of the following has occured:
 * - the specified timeout duration has elapsed,
 * - a location message has been received (could be location or error), or
 * - a signal(7) was received
 * 
 * @param  timeout      The number of milliseconds to wait before
 *                      returning without updating the location
 * @param  location_fd  File descriptor to wait on to receive input event
 */
static void
pull_location(unsigned int timeout, int location_fd)
{
	struct pollfd pollfds[1];
	struct location new_location;
	int r, new_available;

	/* Await new location information */
	pollfds[0].fd = location_fd;
	pollfds[0].events = POLLIN;
	r = poll(pollfds, 1, (int)timeout);
	if (r < 0) {
#ifndef WINDOWS
		if (errno == EINTR)
			return;
#endif
		weprintf("poll:");
		eprintf(_("Unable to get location from provider."));
	} else if (!r) {
		return;
	}

	/* Get new location and availability information */
	if (provider->fetch(provider_state, &new_location, &new_available) < 0)
		eprintf(_("Unable to get location from provider."));
	if (new_available < location_available) {
		weprintf(_("Location is temporarily unavailable; using previous"
			   " location until it becomes available..."));
		location_available = 0;
		return;
	}

	/* Store and announce new location */
	if (new_available > location_available ||
	    !exact_eq(new_location.latitude, location.latitude) ||
	    !exact_eq(new_location.longitude, location.longitude)) {
		location_available = 1;
		location = new_location;
		print_location(&location);
		if (!location_is_valid(&location))
			eprintf(_("Invalid location returned from provider."));
	}
}
#endif


/**
 * Loop for `PROGRAM_MODE_CONTINUAL`
 */
static void
run_continual_mode(void)
{
	enum period period, prev_period = PERIOD_NONE;
	double day_level = FNAN, prev_day_level = FNAN;
	int disabled = disable, prev_disabled = !disable;
	int prev_use_fade = !use_fade;
	int prev_preserve_gamma = !preserve_gamma;
	int done = 0;
	struct colour_setting colour;
	struct colour_setting target_colour, prev_target_colour;
	struct colour_setting fade_start_colour;
	unsigned int fade_length = 0;
	unsigned int fade_time = 0;
	unsigned int delay;
	double fade_progress, eased_fade_progress;
#ifndef WINDOWS
	int location_fd;
	sigset_t sigusr2_mask, old_mask;
	enum signals commands;

	sigemptyset(&sigusr2_mask);
	sigaddset(&sigusr2_mask, SIGUSR2);
#endif

	disable = 0;

	prev_target_colour = COLOUR_SETTING_NEUTRAL;
	colour = COLOUR_SETTING_NEUTRAL;

	for (;;) {
		/* Act on signals */
		if (disable && !done) {
			disabled ^= 1;
			disable = 0;
		}
		if (exiting) {
			disabled = 1;
			exiting = 0;
			if (done || (disabled && !fade_length))
				break; /* On second signal stop the ongoing fade */
			done = 1;
		}
#ifndef WINDOWS
		while (signals) {
			if (sigprocmask(SIG_BLOCK, &sigusr2_mask, &old_mask))
				eprintf("sigprocmask:");
			commands = signals;
			signals = 0;

			if (commands & SIGNAL_ORDER_BARRIER)       sigdelset(&old_mask, SIGUSR2);
			if (commands & SIGNAL_DISABLE)             disabled = 1;
			if (commands & SIGNAL_ENABLE)              disabled = 0;
			if (commands & SIGNAL_RELOAD)              {} /* TODO */
			if (commands & SIGNAL_USE_FADE_OFF)        use_fade = 0;
			if (commands & SIGNAL_USE_FADE_ON)         use_fade = 1;
			if (commands & SIGNAL_PRESERVE_GAMMA_OFF)  preserve_gamma = 0;
			if (commands & SIGNAL_PRESERVE_GAMMA_ON)   preserve_gamma = 1;
			if (commands & SIGNAL_EXIT_WITHOUT_RESET)  {} /* TODO */
			if (commands & SIGNAL_VERBOSE_ON)          verbose |= 2;
			if (commands & SIGNAL_VERBOSE_OFF)         verbose &= ~2;

			if (commands & SIGNAL_IGNORE_SIGPIPE)
				if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
					weprintf("signal SIGPIPE SIG_IGN:");

			if (sigprocmask(SIG_SETMASK, &old_mask, NULL))
				eprintf("sigprocmask:");

# if defined(__linux__)
			if (commands & SIGNAL_REEXEC) { /* TODO */
			}
# endif
		}
#endif
		if (verbose) {
			if (disabled != prev_disabled)
				printf(_("Status: %s\n"), disabled ? _("Disabled") : _("Enabled"));
			if (use_fade != prev_use_fade)
				printf(_("Fade: %s\n"), use_fade ? _("Disabled") : _("Enabled"));
			if (preserve_gamma != prev_preserve_gamma)
				printf(_("Preserve gamma: %s\n"), use_fade ? _("Disabled") : _("Enabled"));
		}
		prev_disabled = disabled;
		prev_use_fade = use_fade;
		prev_preserve_gamma = preserve_gamma;

		/* Get dayness level and corresponding colour settings */
		if (disabled) {
			period = PERIOD_NONE;
			target_colour = COLOUR_SETTING_NEUTRAL;
		} else {
			get_colour_settings(&target_colour, &period, &day_level);
		}
		if (verbose && (period != prev_period || !exact_eq(day_level, prev_day_level)))
			print_period(period, day_level);
		if (period != prev_period)
			run_period_change_hooks(prev_period, period);
		prev_period = period;
		prev_day_level = day_level;
		if (verbose) {
			if (prev_target_colour.temperature != target_colour.temperature)
				printf(_("Color temperature: %luK\n"), target_colour.temperature);
			if (!exact_eq(prev_target_colour.brightness, target_colour.brightness))
				printf(_("Brightness: %.2f\n"), target_colour.brightness);
			if (memcmp(prev_target_colour.gamma, target_colour.gamma, sizeof(target_colour.gamma))) {
				printf(_("Gamma: %.3f, %.3f, %.3f\n"),
				       target_colour.gamma[0], target_colour.gamma[1], target_colour.gamma[2]);
			}
		}

		/* Fade if the parameter differences are too big to apply instantly */
		if (use_fade && colour_setting_diff_is_major(&target_colour, fade_length ? &prev_target_colour : &colour)) {
			fade_length = FADE_LENGTH;
			fade_time = 0;
			fade_start_colour = colour;
		}
		if (fade_length) {
			fade_progress = ++fade_time / (double)fade_length;
			eased_fade_progress = ease_fade(fade_progress);
			interpolate_colour_settings(&fade_start_colour, &target_colour, eased_fade_progress, &colour);
			if (fade_time == fade_length) {
				fade_time = 0;
				fade_length = 0;
			}
		} else {
			colour = target_colour;
		}
		prev_target_colour = target_colour;

		/* Break loop when done and final fade is over */
		if (done && !fade_length)
			break;

		/* Adjust temperature and sleep */
		if (method->apply(method_state, &colour, preserve_gamma) < 0)
			eprintf(_("Temperature adjustment failed."));
		delay = fade_length ? SLEEP_DURATION_SHORT : SLEEP_DURATION;
#ifndef WINDOWS
		location_fd = scheme.type == SOLAR_SCHEME ? provider->get_fd(provider_state) : -1;
		if (location_fd >= 0)
			pull_location(delay, location_fd);
		else
#endif
			millisleep(delay);
		/* SLEEP_DURATION_SHORT is short enough for remaining time after interruption to be ignored */
	}

	method->restore(method_state);
}


int
main(int argc, char *argv[])
{
	struct settings settings;
	double day_level;
	enum period period;
	struct colour_setting colour;
#ifndef WINDOWS
	int fd;
#endif

	argv0 = argv[0];

	/* Set up localisation */
#ifdef ENABLE_NLS
	setlocale(LC_CTYPE, "");
	setlocale(LC_MESSAGES, "");
	bindtextdomain(PACKAGE, LOCALEDIR);
	textdomain(PACKAGE);
#endif

	/* Ensure standard file descriptors exist */
#ifndef WINDOWS
	fd = open("/dev/null", O_RDWR);
	while (fd < 2) {
		if (fd < 0)
			eprintf("open /dev/null O_RDWR:");
		fd = dup(fd);
	}
	if (fd > 2)
		close(fd);
#endif

	/* Set up interprocess communication */
	install_signal_handlers();

	/* Get configurations and configure */
	load_settings(&settings, argc, argv);
	if (scheme.type == SOLAR_SCHEME) {
		acquire_location_provider(&settings, &provider_state);
		provider = settings.provider;
	}
	if (mode != PROGRAM_MODE_PRINT) {
		acquire_adjustment_method(&settings, &method_state);
		method = settings.method;
	}
	config_ini_free(&settings.config);

	/* Get location if required */
	if (scheme.type == SOLAR_SCHEME) {
		if (provider->get_fd(provider_state) >= 0)
			weprintf(_("Waiting for current location to become available..."));
		if (get_location(provider, provider_state, -1, &location) < 0)
			eprintf(_("Unable to get location from provider."));
		if (!location_is_valid(&location))
			eprintf(_("Invalid location returned from provider."));
		print_location(&location);
		location_available = 1;
	}

	/* Get and print colour to set or if continual mode the initial colour */
	get_colour_settings(&colour, &period, &day_level); /* needed in contiual mode for `period` and `day_level` */
	if (mode == PROGRAM_MODE_CONTINUAL)
		colour = COLOUR_SETTING_NEUTRAL;
	if (verbose || mode == PROGRAM_MODE_PRINT) {
		if (scheme.type != STATIC_SCHEME)
			print_period(period, day_level);
		printf(_("Color temperature: %luK\n"), colour.temperature);
		printf(_("Brightness: %.2f\n"), colour.brightness);
		printf(_("Gamma: %.3f, %.3f, %.3f\n"), colour.gamma[0], colour.gamma[1], colour.gamma[2]);
	}

	switch (mode) {
	case PROGRAM_MODE_PRINT:
		break;

	case PROGRAM_MODE_ONE_SHOT:
	case PROGRAM_MODE_UNTIL_DEATH:
	case PROGRAM_MODE_RESET:
		if (method->apply(method_state, &colour, preserve_gamma) < 0)
			eprintf(_("Temperature adjustment failed."));
		if (mode == PROGRAM_MODE_UNTIL_DEATH || method->autoreset) {
			weprintf(_("Press ctrl-c to stop..."));
			while (!exiting) {
				pause();
				if (signals & SIGNAL_EXIT_WITHOUT_RESET) {
					/* TODO disable reset if if using coopgamma */
					goto out;
				}
			}
			/* TODO reset if not using coopgamma */
		}
		break;

	case PROGRAM_MODE_CONTINUAL:
		run_continual_mode();
		break;

#if defined(__GNUC__)
	default:
		__builtin_unreachable();
#endif
	}

out:
	if (provider_state)
		provider->free(provider_state);
	if (method_state)
		method->free(method_state);
	return 0;
}