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


/**
 * Output usage synopsis and exit
 */
static void
usage(void)
{
	fprintf(stderr, _("usage: %s %s\n"), argv0,
	        _("[-b brightness] [-c config-file] [-D | +D] [-E | +E | -e elevations] "
	          "[-g gamma] [-l latitude:longitude | -l provider[:options]] "
	          "[-m method[:options]] [-P | +P] [-r | +r] [-dv] "
	          "[-O temperature | -o | -p | -t temperature | -x] | -h | -V"));
	exit(1);
}


struct colour_setting day_settings;
struct colour_setting night_settings;
union scheme scheme = {.type = SOLAR_SCHEME};
enum program_mode mode = PROGRAM_MODE_CONTINUAL;
int preserve_gamma;
int use_fade;
int verbose = 0;


/**
 * Print general help text
 */
static void
print_help(void) /* TODO clean up; add new options */
{
	/* TRANSLATORS: help output 1
	   LAT is latitude, LON is longitude,
	   DAY is temperature at daytime,
	   NIGHT is temperature at night
	   no-wrap */
	printf(_("Usage: %s -l LAT:LON -t DAY:NIGHT [OPTIONS...]\n"), argv0);
	printf("\n");

	/* TRANSLATORS: help output 2
	   no-wrap */
	printf(_("Set color temperature of display according to time of day.\n"));
	printf("\n");

	/* TRANSLATORS: help output 3
	   no-wrap */
	printf(_("  -h\t\tDisplay this help message\n"
	         "  -v\t\tVerbose output\n"
	         "  -V\t\tShow program version\n"));
	printf("\n");

	/* TRANSLATORS: help output 4
	   `list' must not be translated
	   no-wrap */
	printf(_("  -b DAY:NIGHT\tScreen brightness to apply (between 0.1 and 1.0)\n"
	         "  -c FILE\tLoad settings from specified configuration file\n"
	         "  -g R:G:B\tAdditional gamma correction to apply\n"
	         "  -l LAT:LON\tYour current location\n"
	         "  -l PROVIDER\tSelect provider for automatic location updates\n"
	         "  \t\t(Type `list' to see available providers)\n"
	         "  -m METHOD\tMethod to use to set color temperature\n"
	         "  \t\t(Type `list' to see available methods)\n"
	         "  -o\t\tOne shot mode (do not continuously adjust color temperature)\n"
	         "  -O TEMP\tOne shot manual mode (set color temperature)\n"
	         "  -p\t\tPrint mode (only print parameters and exit)\n"
	         "  -P\t\tReset existing gamma ramps before applying new color effect\n"
	         "  -x\t\tReset mode (remove adjustment from screen)\n"
	         "  -r\t\tDisable fading between color temperatures\n"
	         "  -t DAY:NIGHT\tColor temperature to set at daytime/night\n"));
	printf("\n");

	/* TRANSLATORS: help output 5 */
	printf(_("The neutral temperature is %luK. Using this value will not change the color\n"
	         "temperature of the display. Setting the color temperature to a value higher\n"
	         "than this results in more blue light, and setting a lower value will result in\n"
	         "more red light.\n"),
	       NEUTRAL_TEMPERATURE);

	printf("\n");

	/* TRANSLATORS: help output 6 */
	printf(_("Default values:\n\n"
		 "  Daytime temperature: %luK\n"
		 "  Night temperature: %luK\n"),
	       DEFAULT_DAY_TEMPERATURE, DEFAULT_NIGHT_TEMPERATURE);

	printf("\n");
}


/**
 * Parse a boolean string
 * 
 * @param   s    The string to parse
 * @param   key  The name of the configuration assigned the value `s`
 * @return       1 if `s` represents true, 0 if `s` represents false
 */
GCC_ONLY(__attribute__((__pure__)))
static int
get_boolean(const char *s, const char *key)
{
	int ret;
	if (s[0] == '0' || s[0] == '1') {
		ret = s[0] - '0';
		if (s[1])
			goto bad;
	} else {
	bad:
		eprintf(_("Value of configuration `%s' must be either `1' or `0'."), key);
	}
	return ret;
}


/**
 * atof(3)-like wrapper for strtod(3) that checks that the string is valid
 * 
 * @param   s    The string to parse
 * @param   key  The name of the configuration assigned the value `s`
 * @return       The encoded value
 */
static double
checked_atof(const char *s, const char *key)
{
	double ret;
	errno = 0;
	ret = strtod(s, (void *)&s);
	if (errno || *s)
		eprintf(_("Value of configuration `%s' is not a value decimal value."), key);
	return ret;
}


/**
 * Split a list of values, and remove leading and trailing whitespace
 * from each value
 * 
 * @param   s      The string to split; will be modified
 * @param   count  The number of despired strings
 * @param   strs   Output array for the strings; each element will
 *                 be set to `NULL` or `s` with an offset
 * @param   delim  The character `s` shall be split by, cannot be
 *                 a whitespace character
 * @return         1 if `s` is valid, 0 otherwise
 * 
 * If `count` is 1,
 *     the value most be specified,
 * if `count` is 2,
 *     at least one of values must be specified
 * if `count` is 3,
 *     all values most be specified, otherwise `s` must only contain
 *     on value, and no colon, which will be used from each output value, or
 * if `count` is 6,
 *     `s` must be on one of the formats:
 *         `a`:
 *             the specified value is used for each output value,
 *         `a:b`:
 *             the three lower output values will be set to `a`, which may be empty/`NULL`, and
 *             the three upper output values will be set to `b`, which may be empty/`NULL`;
 *             at least `a` or `b` must be non-empty,
 *         `a:b:c`:
 *             each value most be specified, `s` will be interpreted as `a:b:c:a:b:c`,
 *         `:a:b:c`:
 *             each value most be specified, the lower three values be set to `NULL`,
 *             and `a`, `b`, `c` will be used fo the upper three values,
 *         `a:b:c:`:
 *             each value most be specified, the upper three values be set to `NULL`,
 *             and `a`, `b`, `c` will be used fo the lower three values, or
 *         `a:b:c:d:e:f`:
 *             each value most be specified;
 * where ':' represents `delim`
 * 
 * Summarily said, `s` may contain a scalar value or a 3-tuple, and it may
 * also contain a value or one value for daytime and one value or nighttime.
 * If configured to use 3-tuple but scalar is provided, the provided value is
 * used for each of the 3 requested values. If configured to use daytime and
 * nighttime, but only one is specified it is used for both, but if `s`
 * starts with `delim`, daytime is skipped but if `s` ends with `delim`,
 * nighttime, is skipped; but both cannot be skipped.
 */
static int
get_strings(char *s, int count, char *strs[], char delim)
{
	int i = 0, n;

	/* Split by colon and left-trim */
	for (i = 0; i < count;) {
		strs[i++] = s = ltrim(s);
		s = strchr(s, delim);
		if (!s)
			break;
		*s++ = '\0';
	}
	n = i;

	/* Confirm no excess strings */
	if (s && *ltrim(s))
		return 0;

	/* Right-trim and replace empty strings with NULL */
	for (i = 0; i < n; i++)
		if (!*rtrim(strs[i], NULL))
			strs[i] = NULL;

	/* Validate NULLs */
	switch (n) {
	case 1:
		/* must be specified */
		if (!strs[0])
			return 0;
		break;
	case 2:
		/* at least one most be specified */
		if (!strs[0] && !strs[1])
			return 0;
		break;
	case 3:
		/* each most be specified */
		if (!strs[0] || !strs[1] || !strs[2])
			return 0;
		break;
	case 4:
		/* exactly either the first or the last shall be NULL */
		if (!strs[0] == !strs[3] || !strs[1] || !strs[2])
			return 0;
		break;
	case 6:
		/* each most be specified */
		if (!strs[0] || !strs[1] || !strs[2] || !strs[3] || !strs[4] || !strs[5])
			return 0;
		break;
	default:
		/* n==5 is always invalid */
		return 0;
	}

	/* Duplicate to fill `strs` */
	switch (count) {
	case 2:
		if (n == 1)
			strs[1] = strs[0];
		break;
	case 3:
		if (n == 1)
			strs[2] = strs[1] = strs[0];
		else if (n == 2)
			return 0;
		break;
	case 6:
		if (n == 1) {
			strs[5] = strs[4] = strs[3] = strs[2] = strs[1] = strs[0];
		} else if (n == 2) {
			strs[5] = strs[4] = strs[3] = strs[1];
			strs[2] = strs[1] = strs[0];
		} else if (n == 3) {
			strs[5] = strs[2];
			strs[4] = strs[1];
			strs[3] = strs[0];
		} else if (n == 4 && !strs[0]) {
			strs[5] = strs[3];
			strs[4] = strs[2];
			strs[3] = strs[1];
			strs[2] = NULL;
			strs[1] = NULL;
		} else if (n == 4) {
			strs[5] = NULL;
			strs[4] = NULL;
		}
		break;
	default:
		break;
	}

	return 1;
}


/**
 * Parse and set temperature settings
 * 
 * @param  str    The temperature specification to parse
 * @param  day    The currently specified temperature for daytime,
 *                will be updated; `NULL` if it shall not be set
 * @param  night  The currently specified temperature for nighttime,
 *                will be updated; `NULL` if it shall not be set
 * @param  key    The configuration file setting being parsed,
 *                `NULL` if parsing the command line
 */
static void
set_temperature(char *str, struct setting_lu *day, struct setting_lu *night, const char *key)
{
	struct setting_lu *settings[] = {day, night};
	enum setting_source source = key ? SETTING_CONFIGFILE : SETTING_CMDLINE;
	char *strs[2], *end;
	size_t i, j;

	if (!get_strings(str, !!day + !!night, strs, ':')) {
	invalid:
		weprintf(_("Malformed temperature argument."));
		eprintf(_("Try `-h' for more information."));
	}

	errno = 0;
	for (i = 0, j = 0; i < ELEMSOF(settings); j += settings[i++] ? 1 : 0) {
		if (!settings[i] || !strs[j] || settings[i]->source > source)
			continue;
		if (settings[i]->source & SETTING_CONFIGFILE) {
			if (i == 0)
				weprintf(_("Daytime temperature specified multiple times in configuration file."));
			else
				weprintf(_("Night temperature specified multiple times in configuration file."));
		}
		settings[i]->source |= source;
		settings[i]->value = strtoul(strs[j], &end, 10);
		if (errno || end[*end == 'K'])
			goto invalid;
		if (!WITHIN(MIN_TEMPERATURE, settings[i]->value, MAX_TEMPERATURE))
			eprintf(_("Temperature must be between %luK and %luK."), MIN_TEMPERATURE, MAX_TEMPERATURE);
	}
}


/**
 * Parse and set whitepoint brightness settings
 * 
 * @param  str    The brightness specification to parse
 * @param  day    The currently specified brightness for daytime,
 *                will be updated; `NULL` if it shall not be set
 * @param  night  The currently specified brightness for nighttime,
 *                will be updated; `NULL` if it shall not be set
 * @param  key    The configuration file setting being parsed,
 *                `NULL` if parsing the command line
 */
static void
set_brightness(char *str, struct setting_f *day, struct setting_f *night, const char *key)
{
	struct setting_f *settings[] = {day, night};
	enum setting_source source = key ? SETTING_CONFIGFILE : SETTING_CMDLINE;
	char *strs[2], *end;
	size_t i, j;

	if (!get_strings(str, !!day + !!night, strs, ':')) {
	invalid:
		weprintf(_("Malformed brightness argument."));
		eprintf(_("Try `-h' for more information."));
	}

	errno = 0;
	for (i = 0, j = 0; i < ELEMSOF(settings); j += settings[i++] ? 1 : 0) {
		if (!settings[i] || !strs[j] || settings[i]->source > source)
			continue;
		if (settings[i]->source & SETTING_CONFIGFILE) {
			if (i == 0)
				weprintf(_("Daytime brightness specified multiple times in configuration file."));
			else
				weprintf(_("Night brightness specified multiple times in configuration file."));
		}
		settings[i]->source |= source;
		settings[i]->value = strtod(strs[j], &end);
		if (errno || *end)
			goto invalid;
		if (!WITHIN(MIN_BRIGHTNESS, settings[i]->value, MAX_BRIGHTNESS))
			eprintf(_("Brightness values must be between %.1f and %.1f."), MIN_BRIGHTNESS, MAX_BRIGHTNESS);
	}
}


/**
 * Parse and set gamma settings
 * 
 * @param  str    The gamma specification to parse
 * @param  day    The currently specified gamma for daytime,
 *                will be updated; `NULL` if it shall not be set
 * @param  night  The currently specified gamma for nighttime,
 *                will be updated; `NULL` if it shall not be set
 * @param  key    The configuration file setting being parsed,
 *                `NULL` if parsing the command line
 */
static void
set_gamma(char *str, struct setting_f3 *day, struct setting_f3 *night, const char *key)
{
	struct setting_f3 *settings[] = {day, night};
	enum setting_source source = key ? SETTING_CONFIGFILE : SETTING_CMDLINE;
	char *strs[6], *end;
	size_t i, j, k;

	if (!get_strings(str, 3 * (!!day + !!night), strs, ':')) {
	invalid:
		weprintf(_("Malformed gamma argument."));
		eprintf(_("Try `-h' for more information."));
	}

	errno = 0;
	for (i = 0, j = 0; i < ELEMSOF(settings); j += settings[i++] ? 3 : 0) {
		if (!settings[i] || !strs[j] || settings[i]->source > source)
			continue;
		if (settings[i]->source & SETTING_CONFIGFILE) {
			if (i == 0)
				weprintf(_("Daytime gamma specified multiple times in configuration file."));
			else
				weprintf(_("Night gamma specified multiple times in configuration file."));
		}
		settings[i]->source |= source;
		for (k = 0; k < 3; k++) {
			settings[i]->value[k] = strtod(strs[j + k], &end);
			if (errno || *end)
				goto invalid;
			if (!WITHIN(MIN_GAMMA, settings[i]->value[k], MAX_GAMMA))
				eprintf(_("Gamma values must be between %.1f and %.1f."), MIN_GAMMA, MAX_GAMMA);
		}
	}
}


/**
 * Parse and set solar elevation settings
 * 
 * The fucntion assumes that the setting source is the command line
 * 
 * @param  str    The gamma specification to parse
 * @param  day    The currently specified lowest solar elevation for
 *                daytime, will be updated; `NULL` if it shall not be set
 * @param  night  The currently specified highest solar elevation for
 *                nighttime, will be updated; `NULL` if it shall not be set
 */
static void
set_elevations(char *str, struct setting_f *day, struct setting_f *night)
{
	struct setting_f *settings[] = {day, night};
	const enum setting_source source = SETTING_CMDLINE;
	char *strs[2], *end;
	size_t i, j;

	if (!get_strings(str, !!day + !!night, strs, ':')) {
	invalid:
		weprintf(_("Malformed solar elevation argument."));
		eprintf(_("Try `-h' for more information."));
	}

	errno = 0;
	for (i = 0, j = 0; i < ELEMSOF(settings); j += settings[i++] ? 1 : 0) {
		if (!settings[i] || !strs[j] || settings[i]->source > source)
			continue;
		settings[i]->source |= source;
		settings[i]->value = strtod(strs[j], &end);
		if (errno || *end)
			goto invalid;
	}
}


/**
 * Parse a time string on either of the formats "HH:MM" and "HH:MM:SS"
 * 
 * Times up to, but excluding, 48:00 are supported.
 * 
 * Leap seconds are not supported
 * 
 * @param   str  String to parse
 * @return       The represented time, -1 if malformed
 */
static time_t
parse_time(char *str)
{
	time_t ret;
	unsigned long int v;

	errno = 0;

	if (!isdigit(*str))
		return -1;
	v = strtoul(str, &str, 10);
	if (errno || *str++ != ':' || v >= 48UL)
		return -1;
	ret = (time_t)(v * 60UL * 60UL);

	if (!isdigit(*str))
		return -1;
	v = strtoul(str, &str, 10);
	if (errno || v >= 60UL)
		return -1;
	ret += (time_t)(v * 60UL);

	if (*str) {
		if (*str++ != ':')
			return -1;
		if (!isdigit(*str))
			return -1;
		v = strtoul(str, &str, 10);
		if (errno || *str || v >= 60UL)
			return -1;
		ret += (time_t)v;
	}

	return ret;
}


/**
 * Parse and set a transition time setting
 * 
 * @param   str    The transition time to parse
 * @param   start  The currently specified transition start, will be updated
 * @param   end    The currently specified transition end, will be updated
 * @param   key    The configuration file setting being parsed,
 *                 `NULL` if parsing the command line
 * @return         Normally 1, 0 if `str` is malformeda
 */
static int
set_transition_time(char *str, struct setting_time *start, struct setting_time *end, const char *key)
{
	struct setting_time *settings[] = {start, end};
	enum setting_source source = key ? SETTING_CONFIGFILE : SETTING_CMDLINE;
	char *strs[ELEMSOF(settings)];
	int i;

	if (!get_strings(str, ELEMSOF(strs), strs, '-'))
		return 0;

	for (i = 0; i < (int)ELEMSOF(settings); i++) {
		if (!strs[i] || settings[i]->source > source)
			continue;
		if (settings[i]->source & SETTING_CONFIGFILE) {
			if (i == 0)
				weprintf(_("Start value for `%s' specified multiple times in configuration file."), key);
			else
				weprintf(_("End value for `%s' specified multiple times in configuration file."), key);
		}
		settings[i]->source |= source;
		settings[i]->value = parse_time(strs[i]);
		if (settings[i]->value < 0)
			return 0;
	}

	return 1;
}


/**
 * Print list of available adjustment methods,
 * and some helpful information
 */
static void
print_method_list(void)
{
	size_t i;
	printf(_("Available adjustment methods:\n"));
	for (i = 0; gamma_methods[i]; i++)
		if (gamma_methods[i]->is_available())
			printf("  %s\n", gamma_methods[i]->name);

	printf("\n");
	printf(_("Specify colon-separated options with `-m METHOD:OPTIONS'.\n"));
	/* TRANSLATORS: `help' must not be translated. */
	printf(_("Try `-m METHOD:help' for help.\n"));
}


/**
 * Print list of available location providers,
 * and some helpful information
 */
static void
print_provider_list(void)
{
	size_t i;
	printf(_("Available location providers:\n"));
	for (i = 0; location_providers[i]; i++)
		printf("  %s\n", location_providers[i]->name);

	printf("\n");
	printf(_("Specify colon-separated options with `-l PROVIDER:OPTIONS'.\n"));
	/* TRANSLATORS: `help' must not be translated. */
	printf(_("Try `-l PROVIDER:help' for help.\n"));
}


/**
 * Get adjustment method by name
 *
 * @param   name  The name of the adjustment method to return
 * @return        The adjustment method
 */
GCC_ONLY(__attribute__((__pure__, __returns_nonnull__)))
static const struct gamma_method *
find_gamma_method(const char *name)
{
	size_t i;
	for (i = 0; gamma_methods[i]; i++)
		if (!strcasecmp(name, gamma_methods[i]->name))
			return gamma_methods[i];
	/* TRANSLATORS: This refers to the method used to adjust colours e.g. VidMode */
	eprintf(_("Unknown adjustment method `%s'."), name);
}


/**
 * Get location provider by name
 *
 * @param   name  The name of the location provider to return
 * @return        The location provider
 */
GCC_ONLY(__attribute__((__pure__, __returns_nonnull__)))
static const struct location_provider *
find_location_provider(const char *name)
{
	size_t i;
	for (i = 0; location_providers[i]; i++)
		if (!strcasecmp(name, location_providers[i]->name))
			return location_providers[i];
	eprintf(_("Unknown location provider `%s'."), name);
}


/**
 * Load default settings
 * 
 * @param  settings  Output parameter for the settings
 */
static void
load_defaults(struct settings *settings)
{
	memset(settings, 0, sizeof(*settings)); /* set each `.source` to `SETTING_DEFAULT` and booleans to 0 */

	settings->config_file = NULL;
	settings->scheme_type = UNSPECIFIED_SCHEME;

	settings->day.temperature.value = DEFAULT_DAY_TEMPERATURE;
	settings->day.brightness.value = DEFAULT_DAY_BRIGHTNESS;
	settings->day.gamma.value[0] = DEFAULT_DAY_GAMMA;
	settings->day.gamma.value[1] = DEFAULT_DAY_GAMMA;
	settings->day.gamma.value[2] = DEFAULT_DAY_GAMMA;

	settings->night.temperature.value = DEFAULT_NIGHT_TEMPERATURE;
	settings->night.brightness.value = DEFAULT_NIGHT_BRIGHTNESS;
	settings->night.gamma.value[0] = DEFAULT_NIGHT_GAMMA;
	settings->night.gamma.value[1] = DEFAULT_NIGHT_GAMMA;
	settings->night.gamma.value[2] = DEFAULT_NIGHT_GAMMA;

	settings->preserve_gamma.value = 1;
	settings->use_fade.value = 1;

	settings->elevation_high.value = DEFAULT_HIGH_ELEVATION;
	settings->elevation_low.value = DEFAULT_LOW_ELEVATION;

	settings->method = NULL;
	settings->method_args = NULL;

	settings->provider = NULL;
	settings->provider_args = NULL;
}


/**
 * Load settings from the command line
 * 
 * @param  settings  The currently loaded settings, will be updated
 * @param  argc      Number of command line arguments
 * @param  argv      `NULL` terminated list of command line arguments,
 *                   including argument zero
 */
static void
load_from_cmdline(struct settings *settings, int argc, char *argv[])
{
	const char *provider_name;
	char *s, *end, *value;

	ARGBEGIN {
	case 'b':
		set_brightness(ARG(), &settings->day.brightness, &settings->night.brightness, NULL);
		break;

	case 'c':
		settings->config_file = ARG();
		break;

	case 'D':
		settings->disabled.source |= SETTING_CMDLINE;
		settings->disabled.value = 0;
		break;

	case 'd':
		settings->until_death = 1;
		break;

	case 'E':
		settings->scheme_type = CLOCK_SCHEME;
		break;

	case 'e':
		set_elevations(ARG(), &settings->elevation_high, &settings->elevation_low);
		settings->scheme_type = SOLAR_SCHEME;
		break;

	case 'g':
		set_gamma(ARG(), &settings->day.gamma, &settings->night.gamma, NULL);
		break;

	case 'h':
		print_help();
		exit(0);

	case 'l':
		value = ARG();

		/* Print list of providers if argument is `list' */
		if (!strcasecmp(value, "list")) {
			print_provider_list();
			exit(0);
		}

		provider_name = NULL;

		/* Don't save the result of strtof(); we simply want
		   to know if value can be parsed as a float. */
		errno = 0;
		strtof(value, &end);
		if (!errno && *end == ':') {
			/* Use instead as arguments to `manual'. */
			provider_name = "manual";
			settings->provider_args = value;
		} else {
			/* Split off provider arguments. */
			s = strchr(value, ':');
			if (s) {
				*s++ = '\0';
				settings->provider_args = s;
			}

			provider_name = value;
		}

		/* Lookup provider from name. */
		settings->provider = find_location_provider(provider_name);

		/* Print provider help if arg is `help'. */
		if (settings->provider_args && !strcasecmp(settings->provider_args, "help")) {
			settings->provider->print_help(stdout);
			exit(0);
		}
		break;

	case 'm':
		value = ARG();

		/* Print list of methods if argument is `list' */
		if (!strcasecmp(value, "list")) {
			print_method_list();
			exit(0);
		}

		/* Split off method arguments. */
		s = strchr(value, ':');
		if (s) {
			*s++ = '\0';
			settings->method_args = s;
		}

		/* Find adjustment method by name. */
		settings->method = find_gamma_method(value);

		/* Print method help if arg is `help'. */
		if (settings->method_args && !strcasecmp(settings->method_args, "help")) {
			settings->method->print_help(stdout);
			exit(0);
		}
		break;

	case 'O':
		mode = PROGRAM_MODE_ONE_SHOT;
		set_temperature(ARG(), &settings->day.temperature, &settings->night.temperature, NULL);
		break;

	case 'o':
		mode = PROGRAM_MODE_ONE_SHOT;
		break;

	case 'P':
		settings->preserve_gamma.source |= SETTING_CMDLINE;
		settings->preserve_gamma.value = 0;
		break;

	case 'p':
		mode = PROGRAM_MODE_PRINT;
		break;

	case 'r':
		settings->use_fade.source |= SETTING_CMDLINE;
		settings->use_fade.value = 0;
		break;

	case 't':
		set_temperature(ARG(), &settings->day.temperature, &settings->night.temperature, NULL);
		break;

	case 'V':
		printf("%s\n", VERSION_STRING);
		exit(0);

	case 'v':
		verbose = 1;
		break;

	case 'x':
		mode = PROGRAM_MODE_RESET;
		break;

	default:
		usage();

	} ARGALT('+') {
	case 'D':
		settings->disabled.source |= SETTING_CMDLINE;
		settings->disabled.value = 1;
		break;

	case 'E':
		settings->scheme_type = SOLAR_SCHEME;
		break;

	case 'P':
		settings->preserve_gamma.source |= SETTING_CMDLINE;
		settings->preserve_gamma.value = 1;
		break;

	case 'r':
		settings->use_fade.source |= SETTING_CMDLINE;
		settings->use_fade.value = 1;
		break;

	default:
		usage();
	} ARGEND;

	if (argc)
		usage();
}


/**
 * Load an setting, form the "redshift" section, from the configuration file
 * 
 * @param  settings  The currently loaded settings, will be updated
 * @param  key       The name of the configuration
 * @param  value     The value of the configuration
 */
static void
load_from_config_ini(struct settings *settings, const char *key, char *value)
{
	if (!strcasecmp(key, "temp") || !strcasecmp(key, "temperature")) {
		set_temperature(value, &settings->day.temperature, &settings->night.temperature, key);
	} else if (!strcasecmp(key, "temp-day") || !strcasecmp(key, "temperature-day")) {
		set_temperature(value, &settings->day.temperature, NULL, key);
	} else if (!strcasecmp(key, "temp-night") || !strcasecmp(key, "temperature-night")) {
		set_temperature(value, NULL, &settings->night.temperature, key);

	} else if (!strcasecmp(key, "brightness")) {
		set_brightness(value, &settings->day.brightness, &settings->night.brightness, key);
	} else if (!strcasecmp(key, "brightness-day")) {
		set_brightness(value, &settings->day.brightness, NULL, key);
	} else if (!strcasecmp(key, "brightness-night")) {
		set_brightness(value, NULL, &settings->night.brightness, key);

	} else if (!strcasecmp(key, "gamma")) {
		set_gamma(value, &settings->day.gamma, &settings->night.gamma, key);
	} else if (!strcasecmp(key, "gamma-day")) {
		set_gamma(value, &settings->day.gamma, NULL, key);
	} else if (!strcasecmp(key, "gamma-night")) {
		set_gamma(value, NULL, &settings->night.gamma, key);

	} else if (!strcasecmp(key, "transition")) {
		weprintf(_("`transition' is deprecated and has been replaced with `fade'."));
		goto set_use_fade;
	} else if (!strcasecmp(key, "fade")) {
	set_use_fade:
		if (settings->use_fade.source & SETTING_CONFIGFILE)
			weprintf(_("`fade' setting specified multiple times in configuration file."));
		settings->use_fade.source |= SETTING_CONFIGFILE;
		if (settings->use_fade.source <= SETTING_CONFIGFILE)
			settings->use_fade.value = get_boolean(value, key);

	} else if (!strcasecmp(key, "preserve-gamma")) {
		if (settings->preserve_gamma.source & SETTING_CONFIGFILE)
			weprintf(_("`preserve-gamma' setting specified multiple times in configuration file."));
		settings->preserve_gamma.source |= SETTING_CONFIGFILE;
		if (settings->preserve_gamma.source <= SETTING_CONFIGFILE)
			settings->preserve_gamma.value = get_boolean(value, key);

	} else if (!strcasecmp(key, "start-disabled")) {
		if (settings->disabled.source & SETTING_CONFIGFILE)
			weprintf(_("`start-disabled' setting specified multiple times in configuration file."));
		settings->disabled.source |= SETTING_CONFIGFILE;
		if (settings->disabled.source <= SETTING_CONFIGFILE)
			settings->disabled.value = get_boolean(value, key);

	} else if (!strcasecmp(key, "elevation-high")) {
		if (settings->elevation_high.source & SETTING_CONFIGFILE)
			weprintf(_("`elevation-high' setting specified multiple times in configuration file."));
		settings->elevation_high.source |= SETTING_CONFIGFILE;
		if (settings->elevation_high.source <= SETTING_CONFIGFILE)
			settings->elevation_high.value = checked_atof(value, key);

	} else if (!strcasecmp(key, "elevation-low")) {
		if (settings->elevation_low.source & SETTING_CONFIGFILE)
			weprintf(_("`elevation-low' setting specified multiple times in configuration file."));
		settings->elevation_low.source |= SETTING_CONFIGFILE;
		if (settings->elevation_low.source <= SETTING_CONFIGFILE)
			settings->elevation_low.value = checked_atof(value, key);

	} else if (!strcasecmp(key, "dawn-time")) {
		if (!set_transition_time(value, &settings->dawn_start, &settings->dawn_end, key))
			eprintf(_("Malformed dawn-time setting `%s'."), value);

	} else if (!strcasecmp(key, "dusk-time")) {
		if (!set_transition_time(value, &settings->dusk_start, &settings->dusk_end, key))
			eprintf(_("Malformed dusk-time setting `%s'."), value);

	} else if (!strcasecmp(key, "adjustment-method")) {
		if (!settings->method)
			settings->method = find_gamma_method(value);

	} else if (!strcasecmp(key, "location-provider")) {
		if (!settings->provider)
			settings->provider = find_location_provider(value);

	} else {
		weprintf(_("Unknown configuration setting `%s'."), key);
	}
}


void
load_settings(struct settings *settings, int argc, char *argv[])
{
	struct config_ini_section *section;
	struct config_ini_setting *setting;
	const struct time_period *first, *current;
	int i, j, n;
	time_t duration;

	/* Clear unused bit so they do not interfere with comparsion */
	memset(&day_settings, 0, sizeof(day_settings));
	memset(&night_settings, 0, sizeof(night_settings));

	/* Load settings; some validation takes place */
	load_defaults(settings);
	load_from_cmdline(settings, argc, argv);
	config_ini_init(&settings->config, settings->config_file);
	if ((section = config_ini_get_section(&settings->config, "redshift")))
		for (setting = section->settings; setting; setting = setting->next)
			load_from_config_ini(settings, setting->name, setting->value);

	/* Further validate settings and select scheme */
	n =  !!settings->dawn_start.source + !!settings->dawn_end.source;
	n += !!settings->dusk_start.source + !!settings->dusk_end.source;
	if (settings->scheme_type == SOLAR_SCHEME) {
		n = 0;
	} else if (settings->scheme_type == CLOCK_SCHEME) {
		if (!n)
			eprintf(_("`-E' option used with a time-configuration configured."));
	}
	if (n) {
		scheme.type = CLOCK_SCHEME;
		if (n != 4)
			eprintf(_("Partial time-configuration not supported!"));

		if (settings->dawn_start.value >= ONE_DAY || settings->dusk_start.value >= ONE_DAY ||
		    labs((long)settings->dawn_end.value - (long)settings->dawn_start.value) > (long)ONE_DAY ||
		    labs((long)settings->dusk_end.value - (long)settings->dusk_start.value) > (long)ONE_DAY)
			goto invalid_twilight;

		/* TODO deal with edge-case where one of the twilights last 24 hour */
		settings->dawn_end.value %= ONE_DAY;
		settings->dusk_end.value %= ONE_DAY;
		if (settings->dawn_start.value <= settings->dawn_end.value) {
			if (BETWEEN(settings->dawn_start.value, settings->dusk_start.value, settings->dawn_end.value) ||
			    BETWEEN(settings->dawn_start.value, settings->dusk_end.value, settings->dawn_end.value))
				goto invalid_twilight;
		} else {
			if (!WITHIN(settings->dawn_end.value, settings->dusk_start.value, settings->dawn_start.value) ||
			    !WITHIN(settings->dawn_end.value, settings->dusk_end.value, settings->dawn_start.value))
				goto invalid_twilight;
		}
	}
	if (settings->elevation_high.value < settings->elevation_low.value)
		eprintf(_("High transition elevation cannot be lower than the low transition elevation."));

	/* If resetting effects, use neutral colour settings (static scheme) and do not preserve gamma */
	if (mode == PROGRAM_MODE_RESET) {
		scheme.type = STATIC_SCHEME;
		settings->preserve_gamma.value = 0;
		day_settings = COLOUR_SETTING_NEUTRAL;
		night_settings = COLOUR_SETTING_NEUTRAL;
		goto settings_published;
	}

	/* Publish loaded settings */
	if (mode == PROGRAM_MODE_ONE_SHOT && settings->until_death)
		mode = PROGRAM_MODE_UNTIL_DEATH;
	preserve_gamma = settings->preserve_gamma.value;
	use_fade = settings->use_fade.value;
	disable ^= settings->disabled.value;
	day_settings.temperature = settings->day.temperature.value;
	day_settings.brightness = settings->day.brightness.value;
	day_settings.gamma[0] = settings->day.gamma.value[0];
	day_settings.gamma[1] = settings->day.gamma.value[1];
	day_settings.gamma[2] = settings->day.gamma.value[2];
	night_settings.temperature = settings->night.temperature.value;
	night_settings.brightness = settings->night.brightness.value;
	night_settings.gamma[0] = settings->night.gamma.value[0];
	night_settings.gamma[1] = settings->night.gamma.value[1];
	night_settings.gamma[2] = settings->night.gamma.value[2];
	if (!memcmp(&day_settings, &night_settings, sizeof(day_settings))) {
		/* If the effects are the same throughout the day, do not use a transition scheme */
		scheme.type = STATIC_SCHEME;
	} else if (scheme.type == SOLAR_SCHEME) {
		scheme.elevation.high = settings->elevation_high.value;
		scheme.elevation.low = settings->elevation_low.value;
		scheme.elevation.range = scheme.elevation.high - scheme.elevation.low;
	} else if (scheme.type == CLOCK_SCHEME) {
		scheme.time.periods = &scheme.time.periods_array[0];
		scheme.time.periods_array[0].start = settings->dawn_start.value;
		scheme.time.periods_array[0].day_level = 0.0;
		scheme.time.periods_array[1].start = settings->dawn_end.value;
		scheme.time.periods_array[1].day_level = 1.0;
		scheme.time.periods_array[2].start = settings->dusk_start.value;
		scheme.time.periods_array[2].day_level = 1.0;
		scheme.time.periods_array[3].start = settings->dusk_end.value;
		scheme.time.periods_array[3].day_level = 0.0;
		for (i = 0; i < 4; i++) {
			j = (i + 1) % 4;
			scheme.time.periods_array[i].next = &scheme.time.periods_array[j];
			duration = scheme.time.periods_array[j].start - scheme.time.periods_array[i].start;
			if (duration < 0)
				duration += ONE_DAY;
			scheme.time.periods_array[i].diff_over_duration = scheme.time.periods_array[j].day_level;
			scheme.time.periods_array[i].diff_over_duration -= scheme.time.periods_array[i].day_level;
			scheme.time.periods_array[i].diff_over_duration /= duration ? (double)duration : 1.0;
		}
	}
settings_published:

	/* Output settings */
	if (verbose) {
		if (scheme.type == SOLAR_SCHEME) {
			/* TRANSLATORS: Append degree symbols if possible. */
			printf(_("Solar elevations: day above %.1f, night below %.1f\n"),
			       scheme.elevation.high, scheme.elevation.low);
		} else if (scheme.type == CLOCK_SCHEME) {
			printf(_("Schedule:\n"));
			current = first = scheme.time.periods;
			do {
				printf(_("  %.2f%% day at %02u:%02u:%02u\n"),
				       current->day_level * 100, (unsigned)(current->start / 60 / 60 % 24),
				       (unsigned)(current->start / 60 % 60), (unsigned)(current->start % 60));
			} while ((current = current->next) != first);
			printf(_("(End of schedule)\n"));
		}
		printf(_("Temperatures: %luK at day, %luK at night\n"),
		       day_settings.temperature, night_settings.temperature);
		printf(_("Brightness: %.2f:%.2f\n"), settings->day.brightness.value, settings->night.brightness.value);
		/* TRANSLATORS: The string in parenthesis is either Daytime or Night (translated). */
		printf(_("Gamma (%s): %.3f, %.3f, %.3f\n"), _("Daytime"),
		       day_settings.gamma[0], day_settings.gamma[1], day_settings.gamma[2]);
		printf(_("Gamma (%s): %.3f, %.3f, %.3f\n"), _("Night"),
		       night_settings.gamma[0], night_settings.gamma[1], night_settings.gamma[2]);
	}

	return;

invalid_twilight:
	eprintf(_("Invalid dawn/dusk time configuration!"));
}